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.

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.

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.