S3 vs Supabase Storage vs Cloudinary (2026 Guide)
You're building a Next.js app that needs to store files — user avatars, product photos, PDFs, whatever — and you hit the same wall every developer hits: where do the actual files go?
For most apps, storing large user-uploaded files directly in Postgres isn't the best fit. Object storage is designed for this workload and keeps your database focused on relational data. So you're left picking between three names that show up in almost every tutorial: AWS S3, Supabase Storage, and Cloudinary. They all "store files," but they solve genuinely different problems, and picking the wrong one means either fighting your tools for months or paying for features you'll never touch.
By the end of this guide, you'll know exactly what each one is actually built for, how to wire up a basic upload with real code for all three, what they cost, and which one fits your specific project — not just whichever one showed up first in your search results.
Quick Answer
- Choose AWS S3 if you want inexpensive, highly flexible object storage with a huge ecosystem and full AWS control.
- Choose Supabase Storage if you're already using Supabase for your database and auth — it plugs directly into the same Row Level Security model you already know.
- Choose Cloudinary if images and videos are a core part of your product and you want transformations (resizing, cropping, format conversion, transcoding) handled automatically, with almost no backend code.
If none of those single-sentence answers settled it, keep reading — the "why" behind each one is where the real decision lives.
Why This Comparison Is Tricky
Most "S3 vs X" posts compare feature checklists side by side and call it a day. The problem is these three tools aren't really competing for the same job:
- S3 is raw object storage. It stores bytes under a key. It has no idea what an "image" is — resizing, format conversion, and CDN delivery are all things you have to bolt on separately (usually CloudFront).
- Supabase Storage is an object-storage service with an S3-compatible API, wrapped in Supabase's API, authentication, and Postgres RLS model.
- Cloudinary is a media pipeline first, storage second. Its actual product is on-the-fly image and video transformation — resize, crop, convert to WebP/AVIF, transcode video, remove backgrounds — all driven by URL parameters, with storage as the thing that makes that possible.
Keep that distinction in mind through the rest of this guide: you're not just picking "where files live," you're picking how much of the surrounding work (delivery, optimization, access control) you want to own yourself.
At a Glance
| AWS S3 | Supabase Storage | Cloudinary | |
|---|---|---|---|
| What it actually is | Raw object storage | Object storage + Supabase Auth/RLS integration | Media transformation pipeline |
| Access control model | IAM policies + bucket policies | Row Level Security (SQL policies) | Signed uploads / upload presets |
| Built-in image transforms | None — add an image-processing/optimization layer separately | Available in Paid Plans | Extensive (resize, crop, AI background removal, format negotiation) |
| Video handling | None — you'd wire up a separate transcoding service | Not a core feature | First-class — transcoding, adaptive streaming, a built-in video player |
| CDN included | No — add CloudFront separately | Yes, via the Supabase CDN | Yes, built in |
| Best paired with | Any stack, especially if you already use AWS | A Supabase-backed app (Postgres + Auth) | Any stack where images/video are core UX |
| Free tier | 12-month intro tier only, otherwise pay-as-you-go | ~1 GB storage, 5 GB egress (check current limits) | 25 credits/month (1 credit = 1 GB storage, 1 GB bandwidth, or 1K transformations) credits are shared across these resources |
| Pricing model | Storage + requests + data transfer, billed separately (see below) | Flat plan tiers + usage overages | Credit-based, fixed tiers |
💡 Tip: Pricing on all three changes often enough that any number printed in a blog post is a snapshot, not a promise. Treat the figures above as ballpark, and always check each provider's live pricing page before committing budget to one.
AWS S3: Raw, Cheap, and You Own Everything
Amazon S3 (Simple Storage Service) is the original cloud object store — files go in under a "key" (basically a path string), and you get them back out the same way. No opinions about what the file is.
Where S3 wins:
- One of the cheapest raw storage per GB at real scale, especially once you're past a few hundred GB
- Full control — bucket policies, lifecycle rules (auto-delete or archive old files), versioning, cross-region replication
- The de facto standard — every CDN, backup tool, and CMS speaks S3's API, including S3-compatible alternatives like Cloudflare R2 and Backblaze B2
Where S3 struggles:
- Zero built-in image processing. Want a thumbnail? You'll need a separate image-processing layer, such as a Lambda-based workflow, an image optimization service, or a framework-level optimizer.
- No CDN out of the box — S3 alone serves from one region. You need CloudFront (or another CDN) in front of it for real-world performance.
- More setup: IAM users, bucket policies, CORS config, and (for direct browser uploads) presigned URLs are all things you configure by hand.
S3 Pricing Is More Than "Per GB Stored"
This matters enough for a "which is cheapest" comparison that it deserves its own callout: S3's bill is not just storage. You're billed separately for several things at once:
- Storage — per GB-month, and it varies by storage class (Standard, Infrequent Access, Glacier, etc.)
- Requests —
PUT/COPY/POST/LISTrequests cost more per-request thanGETrequests, and both are metered - Data transfer out — serving files to the internet (not through a CDN) is billed per GB and is usually the line item that surprises people
- Retrieval fees — if you move infrequently-accessed files to a cheaper storage class, pulling them back out costs extra
- Lifecycle transitions — moving objects between storage classes automatically (e.g. Standard → Glacier after 90 days) also incurs a small per-object request cost
A storage-only comparison (S3 vs. Supabase's per-GB rate vs. Cloudinary's credits) misses this. An app that serves thumbnails directly from S3 with heavy traffic and no CDN in front can rack up request and transfer charges that dwarf the storage bill itself — which is exactly why pairing S3 with CloudFront isn't just a performance decision, it's often a cost one too (CloudFront's cached data transfer is typically cheaper than S3's direct transfer-out rate).
A Minimal S3 Upload in Next.js 16
This uses the presigned URL pattern — your server never touches the file bytes, it just asks S3 for permission and hands the browser a temporary signed URL to upload directly.
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
// lib/s3-client.ts
import { S3Client } from "@aws-sdk/client-s3";
export const s3Client = new S3Client({
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
// app/api/presigned-url/route.ts
import { NextRequest, NextResponse } from "next/server";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { s3Client } from "@/lib/s3-client";
import { randomUUID } from "crypto";
import { auth } from "@/lib/auth"; // your auth solution of choice
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"];
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
export async function POST(request: NextRequest) {
// 1. Authentication — reject anonymous requests before doing anything else
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { fileName, fileType, fileSize } = await request.json();
// 2. MIME allowlist — reject anything you don't explicitly support
if (!ALLOWED_TYPES.includes(fileType)) {
return NextResponse.json({ error: "Unsupported file type" }, { status: 400 });
}
// 3. Max file size — a best-effort check against the size the client reports.
// A presigned PUT can't fully enforce this on its own; see the note below.
if (typeof fileSize !== "number" || fileSize > MAX_FILE_SIZE) {
return NextResponse.json({ error: "File too large" }, { status: 400 });
}
// 4. Filename sanitization — never trust the client's filename for anything
// beyond picking a safe extension. Strip everything else.
const safeExtension =
fileName?.split(".").pop()?.replace(/[^a-z0-9]/gi, "").slice(0, 10) || "bin";
// 5. Server-generated key — the client never chooses where its file lands.
// Scoping by the authenticated user's id is also your authorization boundary:
// even if someone tampers with the request, they can only ever get a
// presigned URL scoped to their own prefix.
const key = `uploads/${session.user.id}/${randomUUID()}.${safeExtension}`;
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET_NAME!,
Key: key,
ContentType: fileType,
});
const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 60 });
return NextResponse.json({ uploadUrl, key });
}
What's happening here: your server never sees the file bytes — it just signs a temporary permission slip — but it does gate who gets that slip (authentication), what they're allowed to upload (MIME allowlist), roughly how big it can be, and exactly where it lands (a server-generated key under the caller's own user id, never a client-supplied path). The browser then does a plain fetch(uploadUrl, { method: "PUT", body: file }) straight to S3, keeping large files off your server entirely — which matters a lot on serverless platforms with body-size limits.
⚠️ This example is still a demonstration, not a drop-in production upload endpoint. A few things it deliberately leaves out, worth knowing about before you ship:
- Authoritative size enforcement — the
fileSizecheck above only validates what the client claims its file is. A presignedPutObjectCommanddoesn't enforce a byte limit by itself. For a hard limit, use a presigned POST with acontent-length-rangecondition instead of a presigned PUT, or verify the object's real size after upload (via an S3 event notification or a scheduled cleanup job) and delete anything oversized.- Checksum/content validation — for anything security-sensitive, pass a
ChecksumSHA256toPutObjectCommandso S3 rejects an upload whose bytes don't match what the client claimed, and consider scanning uploaded files (e.g. with a virus-scanning Lambda trigger) before treating them as safe to serve.- Rate limiting — nothing above stops one authenticated user from requesting thousands of presigned URLs per minute. Put a rate limiter (Upstash Ratelimit, a simple sliding-window check in your database, or your edge/CDN's built-in limiting) in front of this route in production.
⚠️ Common Mistake: Making the bucket public "to make things easier." None of the common upload/display patterns need a public bucket — presigned URLs grant scoped, temporary access without exposing the bucket itself. Leave Block all public access on unless you have a specific, narrow reason to open one prefix.
Supabase Storage: S3 Underneath, Postgres on Top
Supabase Storage provides an S3-compatible object-storage interface and integrates it with Supabase Auth and Postgres RLS — every bucket can be protected by Row Level Security policies, the same SQL-based authorization system you're already using for your database tables if you're on Supabase.
Where Supabase Storage wins:
- If you're already using Supabase Auth + Postgres, storage access rules live in the same mental model — a policy like "a user can only read their own files" is one
create policystatement, not a separate IAM configuration - Simple, batteries-included API (
upload,download,createSignedUrl) that works identically from the browser or the server - Offers on-the-fly image transformations included via a query-param API, so you don't always need a separate CDN layer for simple resizing, although availability and usage depend on your plan
Where Supabase Storage struggles:
- Currently supports automatic WebP optimization but its transformation system is considerably narrower than Cloudinary's
- Free-tier storage and egress are modest — fine for an MVP, tight for a media-heavy app
- You're tied to Supabase's infrastructure choices; less low-level control than raw S3
A Minimal Supabase Storage Upload
npm install @supabase/supabase-js @supabase/ssr
// lib/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
);
}
// components/avatar-uploader.tsx
"use client";
import { createClient } from "@/lib/supabase/client";
export function AvatarUploader() {
const supabase = createClient();
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Get the current user from the session — never take a userId as a
// prop or form value. The path below is only as trustworthy as this.
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) return;
const filePath = `${user.id}/${Date.now()}-${file.name}`;
const { error } = await supabase.storage
.from("avatars")
.upload(filePath, file, { upsert: false });
if (error) {
console.error("Upload failed:", error.message);
return;
}
// For a private bucket, generate a temporary signed URL to display it
const { data: signed } = await supabase.storage
.from("avatars")
.createSignedUrl(filePath, 60 * 60); // 1 hour
console.log("Signed URL:", signed?.signedUrl);
};
return <input type="file" accept="image/*" onChange={handleUpload} />;
}
What's happening here: .storage.from("avatars").upload(...) uploads directly through the Supabase client — no separate presigned-URL round trip needed, because auth and authorization already ride along with the client's session. createSignedUrl mirrors S3's presigned-URL pattern, but scoped through the bucket's own RLS policies instead of IAM.
-- Only let a user manage files inside their own folder
create policy "Users can upload their own avatar"
on storage.objects
for insert
to authenticated
with check (
bucket_id = 'avatars'
and (select auth.uid())::text = (storage.foldername(name))[1]
);
What's happening here: storage.foldername(name) splits the object path (e.g. user-id/photo.jpg) into segments, so this policy checks that the folder name matches the uploader's own auth.uid(). The authorization model uses Postgres RLS, just like your database tables.
⚠️ Important — don't mistake a client-supplied path for real security. In the component above,
user.idcomes fromsupabase.auth.getUser(), which reads the server-verified session — that part is trustworthy. But the resultingfilePathstring is still just something the client constructs and sends along with the upload request. What actually makes this safe is the RLS policy, which independently re-checks(select auth.uid())against the path on Supabase's side, every time, regardless of what the client sends.This distinction matters: if you ever accept a user id as a raw prop, a hidden form field, or anything else that isn't derived from the verified session — or if RLS on the bucket is ever disabled or misconfigured — nothing stops a client from writing
someone-elses-id/file.jpginto the path and having the request succeed. The policy is the actual security boundary here, not the fact that the code "usually" builds the path from the right value.
💡 Tip: Writing RLS policies by hand for every bucket pattern (owner-only, team-shared, admin override) gets repetitive fast. If you're already on Supabase, a Supabase RLS Policy Generator can produce the strict, performance-optimized version of these policies — including the
(select auth.uid())wrapper used above — for your specific access pattern.
Cloudinary: A Media Pipeline for Images and Video
Cloudinary's whole pitch is different from the other two: you're not really buying storage, you're buying transformation-on-delivery — and it's easy to think of Cloudinary as "an image tool," but that undersells it. Video is a first-class part of the product too: transcoding, adaptive bitrate streaming, and a dedicated video player component (CldVideoPlayer) ship alongside the image tooling, not as an afterthought. Upload one asset once, and every size, crop, and format you'll ever need — image or video — gets generated from URL parameters, with no separate resize job and no manual WebP conversion.
Where Cloudinary wins:
- Transformations are wildly capable across both media types: resize, crop, auto-format (automatically selects an appropriate optimized format for the requesting browser), AI background removal, text/image overlays for images; transcoding and adaptive streaming for video
- The
next-cloudinarySDK gives you drop-in components —<CldImage>wrapsnext/image,<CldVideoPlayer>handles video — with almost no configuration - Built-in CDN delivery for both, so there's no separate CloudFront-equivalent to wire up
Where Cloudinary struggles:
- Free tier (25 credits/month, where 1 credit ≈ 1 GB storage, 1 GB bandwidth, or 1,000 transformations) disappears fast on anything with real traffic — video in particular eats through credits quickly since files are larger
- Pricing is credit-based across three different resource types at once, which makes forecasting cost harder than a flat per-GB rate
- It's optimized for media-heavy applications; if most of your workload is arbitrary documents and backups, general-purpose object storage is usually a more natural fit
A Minimal Cloudinary Upload + Display in Next.js 16
npm install next-cloudinary cloudinary
// app/api/sign-cloudinary-params/route.ts
import { v2 as cloudinary } from "cloudinary";
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth"; // your auth solution of choice
cloudinary.config({
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
// The widget generates its own paramsToSign and sends them here — never
// sign that object blindly. Only a small, known set of keys is allowed
// through; anything else gets the whole request rejected.
const ALLOWED_PARAM_KEYS = new Set(["timestamp", "folder", "upload_preset", "source"]);
export async function POST(request: Request) {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { paramsToSign } = await request.json();
const keys = Object.keys(paramsToSign ?? {});
const unexpectedKeys = keys.filter((key) => !ALLOWED_PARAM_KEYS.has(key));
if (unexpectedKeys.length > 0) {
return NextResponse.json(
{ error: `Unexpected upload parameters: ${unexpectedKeys.join(", ")}` },
{ status: 400 }
);
}
const signature = cloudinary.utils.api_sign_request(
paramsToSign,
process.env.CLOUDINARY_API_SECRET!
);
return NextResponse.json({ signature });
}
What's happening here: the widget builds paramsToSign on its own based on how it's configured, and sends that exact object to your endpoint to be signed — so you can't simply substitute your own parameters the way you might for a normal API request, or the signature won't match what the widget actually uploads with. What you can do, and what this endpoint does, is refuse to sign anything that isn't on a small allowlist of expected keys, and require a logged-in session before generating a signature at all. This stops the endpoint from being used to sign arbitrary transformation instructions, moderation overrides, or unexpected metadata that a modified client request might try to sneak in. Once signed, the upload still goes directly from the browser to Cloudinary — your CLOUDINARY_API_SECRET never reaches the client.
💡 Tip: Signed uploads narrow who can request a signature, but the file itself — its folder, allowed formats, and max size — is still best constrained by an upload preset configured in the Cloudinary dashboard (Settings → Upload → Upload presets), since a preset's restrictions are enforced by Cloudinary itself and can't be bypassed by tampering with the request the way a loosely-validated
paramsToSignobject could be.
// components/cloudinary-uploader.tsx
"use client";
import { useState } from "react";
import { CldUploadWidget, CldImage } from "next-cloudinary";
export function CloudinaryUploader() {
const [publicId, setPublicId] = useState<string | null>(null);
return (
<div>
<CldUploadWidget
signatureEndpoint="/api/sign-cloudinary-params"
options={{ folder: "uploads" }}
onSuccess={(result) => {
if (result.info && typeof result.info === "object") {
setPublicId(result.info.public_id);
}
}}
>
{({ open }) => (
<button onClick={() => open()}>Upload an image</button>
)}
</CldUploadWidget>
{publicId && (
<CldImage
src={publicId}
width={600}
height={400}
crop="fill"
alt="Uploaded image"
/>
)}
</div>
);
}
What's happening here: <CldImage> renders the uploaded asset with a crop="fill" transformation applied automatically, on top of next/image's own optimization. Swap this component for <CldVideoPlayer> and the same signed-upload flow works for video uploads, using the same signing endpoint.
⚠️ Common Mistake: Using an unsigned upload preset (no
signatureEndpoint, just a public preset name) for anything beyond a quick prototype. Anyone who knows the preset name can initiate uploads subject to the restrictions configured on that preset — fine for a personal demo, risky for anything public-facing.
Media Optimization: Who Actually Handles It?
This is usually the deciding factor in practice, so it's worth calling out directly:
- S3 does nothing for you, for images or video. You either accept unoptimized originals, or build your own pipeline (Lambda@Edge, a CloudFront Function, a dedicated transcoding service, or routing S3 through Cloudinary/imgix as a second layer).
- Supabase Storage offers a transformation API (resize + quality via query params) that works with a custom Next.js image loader — available only in Paid Plans, and images only; there's no equivalent video pipeline.
- Cloudinary is purpose-built for exactly this, for both media types. Automatic format negotiation (AVIF/WebP per browser), on-the-fly cropping,
<CldImage>'s tightnext/imageintegration, and adaptive-bitrate video streaming mean you write the least code here.
If your app's core UX depends on fast, well-optimized media at scale — a marketplace, a photo-heavy blog, a portfolio builder, or a product with video uploads (course platforms, social apps, UGC video) — that alone can justify Cloudinary's cost. If media is incidental — a profile picture here, a document there — S3 or Supabase Storage is plenty.
Security Models Compared
| How access is controlled | |
|---|---|
| S3 | IAM policies (who can call the API) + bucket policies (what's publicly readable) + presigned URLs for temporary, scoped access |
| Supabase Storage | Row Level Security policies written in SQL, evaluated per-request against the logged-in user — the same system protecting your database tables |
| Cloudinary | Signed uploads (server signs the request, ideally after validating it) or unsigned upload presets (public, rate-limited by preset config) |
The practical difference: S3 and Cloudinary both support signed requests for scoped upload/access workflows, while Supabase emphasizes identity-aware RLS policies. Supabase Storage centers on who is asking, evaluated through Postgres RLS policies. If your app already has rich, row-level permission logic in Postgres, Supabase Storage lets you reuse that thinking instead of maintaining a parallel IAM policy language — but in all three, a client-constructed path or parameter is never the actual security boundary on its own; the server-side check (RLS, IAM, or a whitelist) is.
Can You Mix Them?
Yes, and plenty of real apps do. A common pattern:
- S3 (or Cloudflare R2, an S3-compatible alternative) for cheap, high-volume storage of originals — documents, raw uploads, backups
- Cloudinary in front of just the media subset (images and video), either by uploading directly to Cloudinary or using its remote fetch feature to transform images that already live in S3
- Supabase Storage for anything tightly coupled to your Supabase-authenticated user data, where RLS access rules matter more than transformation power
There's no rule that says "pick exactly one." Reach for the tool that fits that specific type of file, not a single storage layer for your entire app by default.
Decision Matrix
| Your situation | Pick |
|---|---|
| Already deep in AWS infrastructure | S3 |
| Need the cheapest storage at real scale, and can account for request/transfer costs | S3 / R2 depending on egress profile |
| Already using Supabase for auth + database | Supabase Storage |
| Want row-level access rules without a separate IAM setup | Supabase Storage |
| Images/video are core to your product's UX | Cloudinary |
| Want automatic format optimization with near-zero setup | Cloudinary |
| Storing mostly non-image files (PDFs, CSVs, backups) | S3 |
| Small MVP, want the fastest path to "it works" | Supabase Storage if the app already uses Supabase; Cloudinary if images or video are central. |
Frequently Asked Questions
Is Supabase Storage actually built on S3?
Supabase Storage is S3-compatible and exposes an S3 protocol endpoint, so tools that speak the S3 API can interact with it. The important distinction is that Supabase adds its own API, authentication integration, and Postgres RLS model.
Do I need Cloudinary if I'm already using next/image?
Not necessarily. next/image already handles resizing, format conversion, and lazy loading for images you serve from S3 or Supabase Storage — it just needs the source domain allowlisted in remotePatterns. Reach for Cloudinary specifically when you need transformations next/image doesn't do itself, like AI background removal, video transcoding, or on-the-fly cropping driven by user input.
Which one is cheapest for a small side project?
For a small MVP, Supabase Storage is attractive if you're already using Supabase, while Cloudinary is attractive when image or video transformations are central. S3 can also be inexpensive, but remember it's billed across storage, requests, and data transfer separately — factor in all three before assuming it's the cheapest option for your specific traffic pattern.
Is a presigned S3 upload URL alone enough security?
No. A presigned URL controls whether an upload to a specific key is allowed within its expiry window — it doesn't validate file type, size, or content on its own beyond what you sign into the request. Pair it with server-side checks (auth, MIME allowlist, size limits) before issuing the URL, as shown in this guide's example, and treat anything beyond that (checksums, virus scanning, rate limiting) as genuinely needed for production, not optional polish.
Can I switch between these later without a full rewrite?
Keeping storage access behind a small abstraction makes migration easier. The files themselves may be easy to migrate; the harder part is usually recreating URLs, access-control rules, transformations, and metadata.
Does Cloudinary replace the need for a CDN?
Yes — Cloudinary delivers every asset through its own CDN by default, so you don't need to add CloudFront or another CDN in front of it the way you would with raw S3.
What about Cloudflare R2 as another alternative?
R2's S3-compatible API makes many application patterns—such as presigned URLs and S3 SDK usage—very similar to S3, although its pricing and access-control model differ.
Wrapping Up
None of these three is a universally "better" choice — they're solving different halves of the same problem. S3 gives you the cheapest, most flexible raw storage and expects you to build the rest, including the parts of its own billing model (requests, transfer, retrieval) that are easy to overlook. Supabase Storage trades some of that flexibility for tight integration with the auth and database system you're probably already using — just remember that a client-supplied path is never the actual security boundary; the RLS policy is. Cloudinary trades storage cost for a transformation pipeline — for images and video — that would otherwise take real engineering time to build yourself.
Start by asking what you're actually storing and who needs to see it — that answer usually points to one of these three faster than a feature-by-feature comparison ever will.
Continue Learning
- Next.js 16 AWS S3 File Upload Guide: Presigned URLs, Proxy & Multipart
- How to Display Images from S3 in Next.js 16 (2026 Guide)
- Build a Todo App with Next.js 16 and Supabase (2026 Guide)
- Image Optimization in Next.js 16: The Complete 2026 Guide
- Supabase Auth vs Firebase Auth: Which One Should You Choose in 2026?
Free Developer Tools
- Supabase RLS Policy Generator — generate strict, pattern-specific Row Level Security policies for your storage buckets, including the owner-only pattern shown in this guide.
More in Backend
View AllZod vs Yup in 2026: Which Validation Library Should You Use?
Zod vs Yup compared for 2026 — TypeScript inference, bundle size, React Hook Form integration, async validation, and real Next.js code examples.
How to Display Images from S3 in Next.js 16 (2026 Guide)
Learn 3 ways to display S3-uploaded images in Next.js 16: public URLs, presigned GET links for private files, and CloudFront CDN delivery.
Next.js 16 AWS S3 File Upload Guide: Presigned URLs, Proxy & Multipart
Learn AWS S3 file uploads in Next.js 16 using server proxy, presigned URLs, and multipart uploads with TypeScript, AWS SDK v3, and production best practices.
Integrate Sanity CMS with Next.js 16 App Router
Integrate Sanity CMS with the Next.js 16 App Router. A complete 2026 guide covering embedded Sanity Studio, GROQ queries, SanityLive, Portable Text, & async params.