Docs · Guides

Object storage & files.

Every project gets a private object-storage bucket for the files your app needs but doesn’t ship in its build — user uploads, generated exports, data dumps. Upload, download, and manage them from the dashboard; the bucket is a Canadian-owned, S3-compatible store.

The project Files tab is a straightforward file manager over a private bucket. It’s the right home for artifacts a build shouldn’t contain: things that change at runtime, are too large to commit, or are produced by the app itself.

Managing files

Open the Files tab on a project to upload a file, download it again, or delete it. The list shows each object’s key, size, and last modified time. Individual uploads are capped by plan, and total bucket usage counts against your plan’s storage quota — Plans & limits has the numbers. When usage reaches the cap, uploads pause until you free space or upgrade, rather than failing unpredictably.

Deleted files and older versions

Deleting a file, or uploading over it, does not destroy the old bytes at once. The Deleted tab on the Files page lists deleted files and files that have older versions, with the date each will be purged. Restore brings a deleted file back (without using extra space), Versions lets you restore an older version of an overwritten file, and Delete forever or Empty deleted removes them immediately and frees the space.

They are kept for 30 days by default. Change it per project on the Deleted tab (1 to 365 days); it is never shorter than the project’s longest database backup or point-in-time window, so a database restored to any retained moment still finds its files. Kept versions count toward your storage, and the meter shows them as their own segment. This applies to files your own code writes with the S3 credentials too. Restoring and purging are recorded in the audit log.

Backing up your files

The Backups tab on the Files page works like database backups: turn on a schedule (how often, how long to keep them, how many) or press Back up now. A file backup lists every file in the project with the version of each, its size and its sha256, and keeps those versions from being purged while the backup exists. Nothing is copied, so a backup adds no storage; the versions it holds show in the meter as their own segment.

Restore puts a version back under the file’s own name, so a reference to the file in your database (its key) keeps working, with no need to restore the database at the same moment. Choose to put back what is missing or changed (nothing is deleted; files made since stay), to restore exactly as of the backup (files made since move to Deleted, where they can be restored), or to copy everything into a _restored folder to look first. Compare with now shows what is missing, changed and added since. Backups are verified on a schedule by reading a sample of the files back and checking each against its sha256; Verify all files reads every one.

Files manifest (.json) downloads the list (key, version, size, sha256, no content), so you can compare it with the file references in your own database. Deleted files are kept at least as long as your longest database backup, so a database restored to any retained moment still finds its files. To remove a file for good, including from backups, use Delete forever in Deleted and choose Also erase from backups; otherwise a version a backup holds is kept until the backup expires. A backup is stored on the same server as the files, so it protects against deletion and overwrites, not against losing the server.

For a copy that survives the loss of this server, choose an off-site destination (one of your own S3-compatible buckets, the same ones database backups can use) under Copy off-site to. Each backup is also copied there as plain, unencrypted files, each distinct file stored once under its sha256, with a readable manifest that maps every file name to its hash, so you can read the copy without Canner. Only content the bucket does not hold yet is uploaded, deleting a backup removes its manifest and any file no other backup needs, and if a file is ever missing here a restore (and a verification) uses the off-site copy.

Private by default

The bucket is private — objects aren’t served on a public URL just because they exist. Files are stored in a Canadian-owned, S3-compatible object store (MinIO), so data residency matches the rest of your app. Keep anything sensitive here rather than in your repository or build output.

Connecting from your own runtime

This is for your app’s server code running on Canner. The bucket’s endpoint sits on the same host your project runs on, so these credentials work from your deployed app’s runtime — not from your laptop, your CI, or any machine outside Canner. There is no public endpoint to point an external S3 client at.

Your app’s own server code connects to the bucket directly — real, S3-compatible credentials, not just the dashboard file manager. Open the Files tab and create a credential under API access. Creating it reveals a ready-to-paste block of environment variables — endpoint, bucket, region, access key ID, and the secret access key — shown once. Add them to your project’s environment variables (they are not injected automatically), then read them as below. The secret cannot be retrieved again — if you lose it, rotate the credential for a new one.

Every credential is scoped to your project only, enforced by MinIO’s own access policy, not just a client-side convention — it cannot list, read, write, or delete objects belonging to any other project, including other projects in your own organization. Object keys must start with your project’s slug; the credential’s policy rejects anything outside that prefix.

import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({
  endpoint: process.env.CANNER_STORAGE_ENDPOINT,
  region: process.env.CANNER_STORAGE_REGION,
  credentials: {
    accessKeyId: process.env.CANNER_STORAGE_ACCESS_KEY_ID,
    secretAccessKey: process.env.CANNER_STORAGE_SECRET_ACCESS_KEY,
  },
  forcePathStyle: true, // MinIO uses path-style addressing, not virtual-hosted-style
});

const bucket = process.env.CANNER_STORAGE_BUCKET;
// Keys must start with your project's slug — that's the prefix your
// credential is scoped to. A key outside it is rejected, not just hidden.
const key = `macapsule-production/certificates/${userId}.pdf`;

await s3.send(new PutObjectCommand({
  Bucket: bucket,
  Key: key,
  Body: pdfBuffer,
  ContentType: 'application/pdf',
}));

// A short-lived signed URL, if you want the browser to fetch the file
// directly instead of proxying it through your own server.
const url = await getSignedUrl(
  s3,
  new GetObjectCommand({ Bucket: bucket, Key: key }),
  { expiresIn: 300 }
);

Need the browser to fetch a file directly? Issue a short-lived presigned URL (the getSignedUrl call above) — a time-limited link to a single private object, so nothing is ever served on a public URL.

Path-style, not virtual-hosted-style: set forcePathStyle: true on your S3 client, or the equivalent option for the SDK you’re using. The connection itself is plain HTTP — it never leaves the host your app runs on, so there’s no public network hop to secure with TLS.

Check that storage is reachable with a list, not HeadBucket: HeadBucket asks about the whole bucket, which your credential is deliberately not allowed to do (the bucket holds your organization’s other projects too), so it answers 403 AccessDenied. Use ListObjectsV2 with Prefix: "your-project-slug/" and MaxKeys: 1 instead; that is allowed and proves the endpoint, the bucket name and the credential are all right. A 400 from a bucket call usually means the bucket name was mistyped: copy it from the credential panel.

Revoke or rotate a credential any time from the same panel — a rotated credential’s old secret stops working immediately, and a revoked one is rejected on its very next request.

What belongs here vs. in your build

  • Object storage: user-uploaded images, generated PDFs and CSVs, backups, anything created or changed after deploy.
  • Your build: static assets that are part of the app itself (logos, bundled JS/CSS) — ship those in the deploy, not the bucket.
  • Structured records: use a Postgres database for rows you query, not files in a bucket.