Photo storage and delivery for photo-sharing apps

Dreep stores every original your users upload and serves each size from the URL. Phones upload directly through presigned URLs your backend issues, photos sit under users/{userId}/, private albums live in signed folders opened by short-lived links, and iPhone HEIC is delivered as WebP or AVIF. Plan limits are fixed and there is no automatic overage billing.

From camera roll to feed

Your backend decides who may upload and how much; Dreep stores, converts and delivers. The bytes never pass through your servers.

  1. User shares 8 photos

    HEIC from iPhone, JPEG from Android

  2. Backend checks quota

    Presigns into albums/{userId}/{albumId}

  3. Phone PUTs bytes

    Single-use URL, 10 minutes

  4. Confirm

    Pending until confirmed

  5. Feed and lightbox

    format=avif, signed if private

Uploads up to 500 MB per file. Image inputs include JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC and HEIF.

Folder layout: public profiles, private albums

Access is set per folder and inherited by every subfolder, so pick two roots and let uploads create the rest on the fly.

PathAccessHoldsServed as
users/{userId}/avatarPublic (root users/)Profile photoPlain URL, e.g. width=128&height=128&radius=max&format=webp
users/{userId}/postsPublic (inherited)Photos shared to the public feedPlain URLs with responsive sizes
albums/{userId}/{albumId}Signed (root albums/)Private and friends-only albumsSigned URLs your backend issues per viewer, e.g. 10 minutes
Use Signed, not Private, for private albums. Private folders are dashboard-only: their assets cannot be fetched through any delivery URL, so your users could never see them.

Private albums and plan-limit handling

Create the Signed root once, presign each upload into the owner's album, and hand viewers signed URLs that carry the size they need.

import { Dreep, DreepLimitError } from "dreep";

const dreep = new Dreep({
  apiKey: process.env.DREEP_API_KEY,
  signingSecret: process.env.DREEP_SIGNING_SECRET,
});

// Once, at setup: albums/ and every folder under it are Signed.
await dreep.createFolder({ path: "albums", accessControlType: "signed", defaultExpirySeconds: 600 });

// POST /api/albums/:albumId/photos/presign (user already authenticated)
export const presignAlbumPhoto = async ({ userId, albumId, filename, contentType, sizeBytes }) => {
  try {
    const upload = await dreep.presignUpload({
      filename,
      contentType, // "image/heic" straight from the phone is fine
      sizeBytes,
      folder: "albums/" + userId + "/" + albumId,
    });
    return { id: upload.id, uploadUrl: upload.uploadUrl };
  } catch (error) {
    if (error instanceof DreepLimitError) {
      // Project plan limit reached: error.featureKey, error.used, error.limit
      return { error: "storage_full" };
    }
    throw error;
  }
};

// Only after checking the viewer may open this album.
export const albumPhotoUrls = ({ asset }) => ({
  thumb: dreep.signedUrl(asset, {
    expiresIn: 600,
    transform: { width: 384, height: 384, fit: "cover", gravity: "attention", format: "avif", quality: 65 },
  }),
  full: dreep.signedUrl(asset, {
    expiresIn: 600,
    transform: { width: 1920, fit: "inside", format: "avif", quality: 75 },
  }),
});

HEIC from phones, WebP and AVIF out

iPhones save photos as HEIC by default, and many browsers cannot display it. Accept the file as uploaded, keep it as the original, and request format=avif or format=webp on every delivery URL. Viewers never see the HEIC file and you never run a conversion job.

For a grid, width=384&height=384&fit=cover&gravity=attention&format=avif&quality=65 gives square thumbnails cropped around the most salient part of the photo. For the lightbox, width=1920&fit=inside keeps the full frame. Add dpr=2 or dpr=3 for high-density screens instead of guessing pixel sizes. Each size is generated on first request, then cached.

Transform on upload, on demand or on the device

Three places to resize a photo, each with a cost. Most photo apps combine a light device-side downscale with on-demand delivery sizes.

ApproachHowGainsCostsUse it for
On uploadtransform on upload or confirm, e.g. width=2560&format=webpSmaller stored file; one transformation per photoThe original is not kept, so you cannot regenerate a larger size laterCapping huge camera files when you never need full resolution
On demandURL parameters or presets at deliveryOriginal kept; add new sizes any time; cached after first requestEach new variant counts towards monthly transformations; cached variants use storageFeeds, grids, lightboxes, share previews
On the deviceResize in the app before uploadSmaller uploads on mobile data; no Dreep quota usedQuality varies by device, costs battery, HEIC handling differs by OS; you still need delivery sizesTrimming 48 MP camera output before it leaves the phone

Predictable quotas, and what happens at the limit

Limits are per project and reset monthly for transformations. Photo counts below assume an average stored original of 2.5 MB and ignore cached variants.

PlanPriceStorageRoughlyImage transformations / month
Free$01 GB400 photos100
Starter$2.99/month10 GB4,000 photos10,000
Pro$5.99/month50 GB20,000 photos50,000
Add-ons+100 GB $6.99/month; +100k transformations $4.99 onceStackable+40,000 photos per 100 GBOne-time packs

When a limit is reached, the request fails with LIMIT_EXCEEDED (HTTP 402); the JavaScript SDK throws DreepLimitError with featureKey, used and limit, and the Go SDK exposes the same through dreep.FeatureLimit. Nothing is billed automatically. You buy an add-on or upgrade, and requests succeed again.

Dreep enforces limits for the whole project, not per end user. Keep per-user quotas in your own database, using sizeBytes from each confirmed asset, and poll GET /usage so you can alert at 80% rather than discover the limit from a failed upload.

What each feature does for a photo app

Presigned uploads

Photos go from the phone to Dreep without touching your servers, so upload spikes after a holiday weekend do not need more backend capacity.

Inherited folder access

A new album folder created by an upload is Signed because albums/ is Signed. No per-album setup, no accidental public album.

Format on delivery

HEIC, JPEG and PNG originals all leave as AVIF or WebP, chosen by a URL parameter rather than a conversion pipeline.

Fixed limits

A viral week cannot produce an open-ended bill. It produces a LIMIT_EXCEEDED error you can catch and act on.

Who this is for

  • Photo-sharing, family album and community apps with user uploads from phones.
  • Apps that need private or friends-only albums served through expiring links.
  • Teams that want a fixed monthly cost and explicit errors instead of overage charges.
  • Apps whose users upload HEIC and view on any browser.

When Dreep is not the right fit

  • You need automatic nudity or abuse detection. Dreep does not moderate content; add a moderation step.
  • You need face recognition or people tagging. Dreep only offers attention-based cropping.
  • You store many terabytes on managed storage. Plan tiers here are sized in gigabytes; look at Bring Your Own Bucket or a storage-first provider.

Frequently asked questions

Can users upload HEIC photos straight from an iPhone?

Yes. HEIC and HEIF are accepted inputs. Store the file as uploaded and request format=avif or format=webp when you display it.

Should private albums use Private or Signed folders?

Signed. Private folders can only be viewed by project members in the Dreep dashboard, so your users could never open them. Signed folders are fetched through time-limited signed URLs your backend generates.

How long should a signed album link last?

As short as your UI allows, often 5 to 15 minutes for in-app viewing. Anyone holding a signed URL can open it until it expires, so treat it as a bearer token. Signing is local to the SDK, so issuing fresh links costs no API call.

How do I limit how much each user can upload?

In your backend, before you presign. Dreep enforces plan limits per project, not per end user. Record sizeBytes from each confirmed upload and refuse to presign when a user is over their allowance.

Do thumbnails count against my plan?

Yes. Generating a new size counts towards monthly image transformations, and cached variants take up storage. Fewer, consistent sizes keep both numbers down, which is what presets are for.

How do I delete everything when a user closes their account?

Walk the user's folders with listAllMedia (or ListAllMedia in Go) and delete each asset. Deleting removes the original and every cached transform, and credits the space back.

Related

Build a private album flow on the free plan

Create a Signed albums folder, upload a HEIC photo from your phone and open it through a signed AVIF link. 1 GB and 100 transformations are free.