Skip to content
smallPict
Start Free

Bring Your Own Storage + CDN

Connect your own object storage and your own CDN on the API Velocity and Momentum plans: setup per provider, minimal permissions, the connection check, and what happens when you switch modes.

Available on: the API Velocity and Momentum plans. Not available on Ignite (it has no CDN), in the developer sandbox (sandbox files are temporary), or on WordPress plans (they use the managed smallPict CDN).

In short: In BYO mode, smallPict writes your originals and optimized images to your bucket, and your CDN serves them. Storage and CDN are always connected together: you can't bring one without the other.


What BYO is

Velocity and Momentum have two delivery modes. You pick one in Dashboard → CDN & storage.

  • Managed (the default): smallPict keeps your originals in its encrypted storage (moved to long-term archive after 90 days) and serves optimized images from cdn.smallpict.app. Nothing to set up.
  • Bring your own storage + CDN (BYO): you connect an S3-compatible bucket in your own account and a CDN in your own account. Every new job writes the original and the optimized file to your bucket, and the API returns URLs on your CDN domain.

The coupled rule exists because smallPict has to be able to do both halves of delivery: write files and clear stale copies from the cache. A bucket without a CDN would leave cache purges with nowhere to go; a CDN without a bucket would have nothing to serve.


Managed vs. BYO

Managed (default)Bring your own storage + CDN
OriginalssmallPict encrypted storageoriginals/<job_id>/<file> in your bucket
Optimized filesServed from cdn.smallpict.appoptimized/<job_id>.<ext> in your bucket, served from your CDN domain
Storage quotaVelocity 50 GB, Momentum 100 GBNot counted. You pay your storage provider.
CDN bandwidthVelocity 30 GB/month, Momentum 200 GB/monthNot counted. You pay your CDN provider.
TransformationsCount toward your planCount toward your plan
Copy kept by smallPictYes, while your account is activeNone. Only the temporary processing upload, deleted within 24 hours.
Cache purgeAutomaticAutomatic, through your CDN's API
CDN performance, domain and costssmallPictYou. smallPict is not responsible for your CDN's performance, domains or bills.

Supported providers

Storage (available today): Amazon S3, Cloudflare R2, Google Cloud Storage (S3 interoperability), Alibaba Cloud OSS, Tencent Cloud COS, DigitalOcean Spaces, SumoPod Storage, MinIO, and any other S3-compatible service through a custom endpoint (for example Hetzner, Vultr or Wasabi).

CDN (available today): Cloudflare and Amazon CloudFront.

Planned: Azure Blob Storage and other CDNs. You can ask for early access on the Cloud providers page.


Before you start

  1. You are on API Velocity or Momentum and signed in as an account admin.
  2. You have a bucket, and a CDN that serves files from that bucket over HTTPS.
  3. The storage endpoint is reachable from the internet over HTTPS. Private, internal and plain-HTTP endpoints are rejected.
  4. You have created narrow keys for smallPict (see Minimal permissions below). Don't use your account's root or admin keys.

Then open Dashboard → CDN & storage, choose Bring your own storage + CDN, fill in both parts and select Save storage + CDN. smallPict runs the connection check (described below) and switches you to BYO only if it passes.


Setup guides

A. Amazon S3 + Amazon CloudFront

  1. Create the bucket in the region you want, for example my-images in ap-southeast-1. Keep Block Public Access on.
  2. Create a CloudFront distribution with the bucket as its origin. Use Origin access control (OAC) so CloudFront can read the bucket while it stays private, and apply the bucket policy CloudFront offers you.
  3. Optional: your own domain. Add an alternate domain name (for example images.example.com) and a certificate to the distribution, and point a DNS record at the distribution.
  4. Create an IAM user (or role) for storage with this policy:
JSON
{  "Version": "2012-10-17",  "Statement": [    {      "Effect": "Allow",      "Action": [        "s3:PutObject",        "s3:GetObject",        "s3:DeleteObject"      ],      "Resource": "arn:aws:s3:::my-images/*"    }  ]}
  1. Create an IAM user for CloudFront (it can be the same user) with this policy. cloudfront:GetDistribution is optional; with it, smallPict can detect the distribution's domain when you leave the CDN domain empty.
JSON
{  "Version": "2012-10-17",  "Statement": [    {      "Effect": "Allow",      "Action": [        "cloudfront:CreateInvalidation",        "cloudfront:GetDistribution"      ],      "Resource": "arn:aws:cloudfront::123456789012:distribution/E2QWRUHAPOMQZL"    }  ]}
  1. In the dashboard:
    • Storage provider Amazon S3, bucket my-images, region ap-southeast-1, endpoint empty (the standard endpoint for the region is used), and the storage access key ID and secret.
    • CDN Amazon CloudFront, the distribution ID (for example E2QWRUHAPOMQZL), the CloudFront access key ID and secret, and optionally the CDN domain (https://images.example.com). Leave the domain empty to use the distribution's own *.cloudfront.net domain.

B. Cloudflare R2 + Cloudflare

  1. Create an R2 bucket, for example my-images.
  2. Connect a custom domain to the bucket (R2 → your bucket → Settings → Custom Domains), for example images.example.com, on a zone in the same Cloudflare account. Requests to that domain go through the Cloudflare cache. Don't use the r2.dev development URL for production.
  3. Create an R2 API token (R2 → Manage API tokens) with Object Read & Write, scoped to this bucket only. Copy the Access Key ID and Secret Access Key it shows.
  4. Create a Cloudflare API token (My Profile → API Tokens) for the zone with Zone → Cache Purge → Purge. Zone → Zone → Read is optional.
  5. In the dashboard:
    • Storage provider Cloudflare R2, bucket my-images, endpoint https://<account_id>.r2.cloudflarestorage.com (shown in your bucket's S3 API details), region auto, and the R2 Access Key ID and Secret Access Key.
    • CDN Cloudflare, the zone ID (Overview page of the domain, API section), the API token, and the CDN domain https://images.example.com.

C. Google Cloud Storage + Cloudflare

Google Cloud Storage is connected through its S3-compatible XML API with HMAC keys.

  1. Create the bucket, for example my-images.
  2. Create a service account and grant it Storage Object User (roles/storage.objectUser) on this bucket only.
  3. Create an HMAC key for the service account: Cloud Storage → Settings → Interoperability → Create a key for a service account. Copy the access ID and secret.
  4. Make the optimized files readable by your CDN. Cloudflare fetches files from Cloud Storage over HTTPS, so objects under optimized/ must be publicly readable. With uniform bucket-level access, granting allUsers the Storage Object Viewer role makes the whole bucket readable, originals/ included. If your originals must stay private, put an authenticating layer in front (for example a Cloudflare Worker that signs requests to the bucket).
  5. Point a proxied Cloudflare DNS record at Cloud Storage, for example images.example.com. Either name the bucket after the hostname and use a proxied CNAME to c.storage.googleapis.com, or use a Cloudflare Origin Rule that sends requests to storage.googleapis.com with the bucket name at the start of the path.
  6. Create a Cloudflare API token for the zone with Zone → Cache Purge → Purge (Zone → Zone → Read optional).
  7. In the dashboard: storage provider Google Cloud Storage, bucket, endpoint https://storage.googleapis.com, region auto, the HMAC access ID and secret; CDN Cloudflare, zone ID, API token and CDN domain https://images.example.com.

D. MinIO or any S3-compatible storage + Cloudflare

This covers MinIO and S3-compatible services such as Hetzner, Wasabi, Vultr, SumoPod, Alibaba Cloud OSS, Tencent Cloud COS and DigitalOcean Spaces.

ProviderChoose in the dashboardEndpointNotes
MinIOMinIOYour server, e.g. https://minio.example.comPath-style requests on. The server must be reachable from the internet over HTTPS.
Hetzner Object StorageCustom S3-compatiblehttps://<location>.your-objectstorage.com
WasabiCustom S3-compatiblehttps://s3.<region>.wasabisys.com
Vultr Object StorageCustom S3-compatiblehttps://<region>.vultrobjects.com
SumoPod StorageSumoPod StorageFrom your SumoPod storage dashboardPath-style on by default.
Alibaba Cloud OSSAlibaba Cloud OSShttps://oss-<region>.aliyuncs.comVirtual-hosted style only.
Tencent Cloud COSTencent Cloud COShttps://cos.<region>.myqcloud.comThe bucket name includes your APPID, e.g. my-images-1250000000.
DigitalOcean SpacesDigitalOcean Spaceshttps://<region>.digitaloceanspaces.com
  1. Create the bucket and a key limited to that bucket with put, get and delete on objects (see Minimal permissions below).
  2. Make optimized/ readable by your CDN, for example with a bucket policy that allows anonymous s3:GetObject on optimized/* only. On MinIO:
JSON
{  "Version": "2012-10-17",  "Statement": [    {      "Effect": "Allow",      "Principal": {        "AWS": [          "*"        ]      },      "Action": [        "s3:GetObject"      ],      "Resource": [        "arn:aws:s3:::my-images/optimized/*"      ]    }  ]}
  1. Put Cloudflare in front of the bucket: a proxied DNS record for images.example.com that points at the bucket's public host. If the provider needs its own hostname in the request, add a Cloudflare Origin Rule that rewrites the Host header (and, for path-style hosts, adds the bucket name to the path).
  2. Create a Cloudflare API token for the zone with Zone → Cache Purge → Purge (Zone → Zone → Read optional).
  3. In the dashboard: pick the provider from the table, enter the bucket, endpoint, region (auto if the provider has none) and keys; turn on path-style requests where the table says so; then CDN Cloudflare, zone ID, API token and CDN domain.

Minimal permissions

Give smallPict keys that can only do what it needs, on one bucket.

ProviderPermissions
Amazon S3s3:PutObject, s3:GetObject, s3:DeleteObject on arn:aws:s3:::<bucket>/*
Amazon CloudFrontcloudfront:CreateInvalidation on the distribution; optional cloudfront:GetDistribution so the domain can be detected
Cloudflare R2R2 API token with Object Read & Write, scoped to the bucket
Cloudflare (CDN)API token for the zone with Zone → Cache Purge → Purge; optional Zone → Zone → Read
Google Cloud StorageHMAC key for a service account with roles/storage.objectUser on the bucket
Alibaba Cloud OSSRAM user with oss:PutObject, oss:GetObject, oss:DeleteObject on the bucket
Tencent Cloud COSCAM sub-user with cos:PutObject, cos:GetObject, cos:DeleteObject on the bucket (the bucket name includes the APPID)
DigitalOcean SpacesSpaces access key limited to the bucket with read, write and delete
MinIO / custom S3-compatiblePut, get and delete on <bucket>/*; the endpoint must be public HTTPS

The connection check

Every time you save (and when you select Retry check), smallPict checks the whole connection before it uses it:

  1. Storage: writes a small test file under .smallpict-probe/ in your bucket, reads it back and deletes it.
  2. Cloudflare: sends one single-URL test purge to your zone.
  3. CloudFront: creates one test invalidation for a /.smallpict-probe/... path. It counts toward your CloudFront invalidation paths for the month.

If any step fails, nothing is switched. The dashboard shows the reason next to the field it concerns, for example the bucket name or the API token. The connection check is limited to 5 attempts per minute.


Bucket access for your CDN

  • optimized/ must be readable by your CDN: either public-read on that prefix, or CDN origin access to a private bucket (CloudFront origin access control, or an R2 custom domain).
  • originals/ can stay private. Your CDN never needs it.
  • CORS is needed only if browsers fetch the images cross-origin from JavaScript (for example fetch() or a canvas). Plain <img> tags don't need it. If you do need it, allow GET and HEAD from your site's origin.

Object layout and URLs

WhatKey in your bucketURL
Originaloriginals/<job_id>/<file>Not served
Optimized fileoptimized/<job_id>.<ext><cdn_domain>/optimized/<job_id>.<ext>

The CDN domain can include a path prefix, for example https://example.com/images; the URL is then https://example.com/images/optimized/<job_id>.<ext>.


Cache headers

Optimized files are written with Cache-Control: public, max-age=31536000, immutable. Every job gets a unique key, so a new result always has a new URL and purging is rarely needed. Keep your CDN's caching rules from overriding this header with a shorter time.


Cache purge

  • Purge requests made through the API (POST /v1/purge) go to your CDN, batched.
  • On Cloudflare, files are purged by URL. "Purge all" purges only your delivery host and path prefix, not your whole zone.
  • On CloudFront, files are purged with invalidations. "Purge all" creates a /* invalidation. Invalidation paths beyond CloudFront's monthly free allowance are billed by AWS to your account.

Switching modes

Switching modes does not move existing files. Files already delivered keep their current URLs; only new jobs use the new mode.

  • Managed → BYO: new jobs go to your bucket and are served by your CDN. Files already on cdn.smallpict.app stay there.
  • BYO → managed: new jobs use managed storage and CDN again. Your BYO settings and keys stay saved but unused, so you can switch back without entering them again, until you select Disconnect.
  • Disconnect: deletes the saved keys and BYO settings and returns you to managed. Files in your bucket are not touched.

When something fails

  • If smallPict can't write to your bucket, it retries, then fails the job with a reason you can read in the API response.
  • The dashboard shows the last error and when it happened, with a Retry check button.
  • You get one email per incident, not one per failed job.
  • smallPict never falls back to managed storage. Your files are never stored somewhere you didn't choose.

Security

  • Keys are kept in an encrypted secrets store. They are never shown again, never returned by the API and never written to logs.
  • To rotate a key, enter the new one and save; the connection check runs with the new key. Leave a key field empty to keep the saved key.
  • Storage and CDN endpoints must use HTTPS. Endpoints that resolve to private, loopback, link-local or other internal addresses are rejected.
  • Use separate, narrow keys for smallPict, and revoke them in your provider's console if you stop using BYO.