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 | |
|---|---|---|
| Originals | smallPict encrypted storage | originals/<job_id>/<file> in your bucket |
| Optimized files | Served from cdn.smallpict.app | optimized/<job_id>.<ext> in your bucket, served from your CDN domain |
| Storage quota | Velocity 50 GB, Momentum 100 GB | Not counted. You pay your storage provider. |
| CDN bandwidth | Velocity 30 GB/month, Momentum 200 GB/month | Not counted. You pay your CDN provider. |
| Transformations | Count toward your plan | Count toward your plan |
| Copy kept by smallPict | Yes, while your account is active | None. Only the temporary processing upload, deleted within 24 hours. |
| Cache purge | Automatic | Automatic, through your CDN's API |
| CDN performance, domain and costs | smallPict | You. 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
- You are on API Velocity or Momentum and signed in as an account admin.
- You have a bucket, and a CDN that serves files from that bucket over HTTPS.
- The storage endpoint is reachable from the internet over HTTPS. Private, internal and plain-HTTP endpoints are rejected.
- 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
- Create the bucket in the region you want, for example
my-imagesinap-southeast-1. Keep Block Public Access on. - 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.
- 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. - Create an IAM user (or role) for storage with this policy:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:DeleteObject" ], "Resource": "arn:aws:s3:::my-images/*" } ]}- Create an IAM user for CloudFront (it can be the same user) with this policy.
cloudfront:GetDistributionis optional; with it, smallPict can detect the distribution's domain when you leave the CDN domain empty.
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "cloudfront:CreateInvalidation", "cloudfront:GetDistribution" ], "Resource": "arn:aws:cloudfront::123456789012:distribution/E2QWRUHAPOMQZL" } ]}- In the dashboard:
- Storage provider Amazon S3, bucket
my-images, regionap-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.netdomain.
- Storage provider Amazon S3, bucket
B. Cloudflare R2 + Cloudflare
- Create an R2 bucket, for example
my-images. - 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 ther2.devdevelopment URL for production. - 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.
- Create a Cloudflare API token (My Profile → API Tokens) for the zone with Zone → Cache Purge → Purge. Zone → Zone → Read is optional.
- In the dashboard:
- Storage provider Cloudflare R2, bucket
my-images, endpointhttps://<account_id>.r2.cloudflarestorage.com(shown in your bucket's S3 API details), regionauto, 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.
- Storage provider Cloudflare R2, bucket
C. Google Cloud Storage + Cloudflare
Google Cloud Storage is connected through its S3-compatible XML API with HMAC keys.
- Create the bucket, for example
my-images. - Create a service account and grant it Storage Object User (
roles/storage.objectUser) on this bucket only. - 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.
- 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, grantingallUsersthe 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). - 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 toc.storage.googleapis.com, or use a Cloudflare Origin Rule that sends requests tostorage.googleapis.comwith the bucket name at the start of the path. - Create a Cloudflare API token for the zone with Zone → Cache Purge → Purge (Zone → Zone → Read optional).
- In the dashboard: storage provider Google Cloud Storage, bucket, endpoint
https://storage.googleapis.com, regionauto, the HMAC access ID and secret; CDN Cloudflare, zone ID, API token and CDN domainhttps://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.
| Provider | Choose in the dashboard | Endpoint | Notes |
|---|---|---|---|
| MinIO | MinIO | Your server, e.g. https://minio.example.com | Path-style requests on. The server must be reachable from the internet over HTTPS. |
| Hetzner Object Storage | Custom S3-compatible | https://<location>.your-objectstorage.com | |
| Wasabi | Custom S3-compatible | https://s3.<region>.wasabisys.com | |
| Vultr Object Storage | Custom S3-compatible | https://<region>.vultrobjects.com | |
| SumoPod Storage | SumoPod Storage | From your SumoPod storage dashboard | Path-style on by default. |
| Alibaba Cloud OSS | Alibaba Cloud OSS | https://oss-<region>.aliyuncs.com | Virtual-hosted style only. |
| Tencent Cloud COS | Tencent Cloud COS | https://cos.<region>.myqcloud.com | The bucket name includes your APPID, e.g. my-images-1250000000. |
| DigitalOcean Spaces | DigitalOcean Spaces | https://<region>.digitaloceanspaces.com |
- Create the bucket and a key limited to that bucket with put, get and delete on objects (see Minimal permissions below).
- Make
optimized/readable by your CDN, for example with a bucket policy that allows anonymouss3:GetObjectonoptimized/*only. On MinIO:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": [ "*" ] }, "Action": [ "s3:GetObject" ], "Resource": [ "arn:aws:s3:::my-images/optimized/*" ] } ]}- Put Cloudflare in front of the bucket: a proxied DNS record for
images.example.comthat 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). - Create a Cloudflare API token for the zone with Zone → Cache Purge → Purge (Zone → Zone → Read optional).
- In the dashboard: pick the provider from the table, enter the bucket, endpoint, region (
autoif 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.
| Provider | Permissions |
|---|---|
| Amazon S3 | s3:PutObject, s3:GetObject, s3:DeleteObject on arn:aws:s3:::<bucket>/* |
| Amazon CloudFront | cloudfront:CreateInvalidation on the distribution; optional cloudfront:GetDistribution so the domain can be detected |
| Cloudflare R2 | R2 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 Storage | HMAC key for a service account with roles/storage.objectUser on the bucket |
| Alibaba Cloud OSS | RAM user with oss:PutObject, oss:GetObject, oss:DeleteObject on the bucket |
| Tencent Cloud COS | CAM sub-user with cos:PutObject, cos:GetObject, cos:DeleteObject on the bucket (the bucket name includes the APPID) |
| DigitalOcean Spaces | Spaces access key limited to the bucket with read, write and delete |
| MinIO / custom S3-compatible | Put, 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:
- Storage: writes a small test file under
.smallpict-probe/in your bucket, reads it back and deletes it. - Cloudflare: sends one single-URL test purge to your zone.
- 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, allowGETandHEADfrom your site's origin.
Object layout and URLs
| What | Key in your bucket | URL |
|---|---|---|
| Original | originals/<job_id>/<file> | Not served |
| Optimized file | optimized/<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.appstay 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.