Skip to main content
Modelence provides built-in file storage through Modelence Cloud. Files are organized by visibility — either public (accessible via a permanent URL) or private (accessible only via a time-limited signed URL).
File operations are server-only. getUploadUrl, getFileUrl, downloadFile, and deleteFile are imported from modelence/server and can only be called from your server-side code (queries, mutations, cron jobs). They are not callable directly from the browser.This is deliberate. Each operation issues a signed URL or performs a delete against Modelence Cloud using your app’s credentials, for whatever filePath it is given. The framework has no way to know which user owns which file, so exposing these to the client would let any caller act on any path. You decide who can touch which file by wrapping these calls in your own queries/mutations and enforcing ownership there — see Exposing file operations to the client.

Uploading a File

getUploadUrl returns a presigned URL and form fields. POST these together as FormData to upload the file directly to storage:
import { getUploadUrl } from 'modelence/server';
import fs from 'fs/promises';

const { url, fields, filePath } = await getUploadUrl({
  filePath: 'photo.png',
  contentType: 'image/png',
  visibility: 'public',
});

const data = await fs.readFile('./photo.png');
const formData = new FormData();
for (const [key, value] of Object.entries(fields)) {
  formData.append(key, value);
}
formData.append('file', new Blob([data], { type: 'image/png' }));

await fetch(url, { method: 'POST', body: formData });

console.log(filePath); // e.g. 'public/photo.png'
The visibility field controls access:
  • "public" — the file is accessible via a permanent URL
  • "private" — the file requires a signed URL to access

Getting a File URL

Returns a URL for displaying or linking to a file. For public files this is a permanent URL; for private files it is a time-limited presigned URL.
import { getFileUrl } from 'modelence/server';

const { url } = await getFileUrl('public/photo.png');
console.log(url); // Permanent public URL

const { url: privateUrl } = await getFileUrl('private/report.pdf');
console.log(privateUrl); // Time-limited presigned URL

Downloading a File

Returns a presigned download URL for a private file.
import { downloadFile } from 'modelence/server';

const { downloadUrl } = await downloadFile('private/report.pdf');
// Redirect the user to downloadUrl or fetch it server-side

Deleting a File

import { deleteFile } from 'modelence/server';

await deleteFile('public/photo.png');
await deleteFile('private/report.pdf');

File Paths

File paths always include the visibility prefix: public/<path> or private/<path>. The upload functions return the full filePath (including prefix) in their result, which you can store and pass to the other functions.
import { getUploadUrl } from 'modelence/server';

const { filePath } = await getUploadUrl({
  filePath: 'avatars/user-123.png',
  contentType: 'image/png',
  visibility: 'private',
});
// filePath === 'private/avatars/user-123.png'

const { url } = await getFileUrl(filePath);

Exposing file operations to the client

Because the file functions are server-only, the browser reaches them through your own queries and mutations. This is where you enforce authorization: never accept a raw client-supplied filePath and forward it blindly — derive the path from your own data and verify the current user is allowed to act on it. The pattern below stores file ownership in a Store and checks it on every operation. Uploading returns the presigned URL to the client, which performs the actual upload directly to storage.
import { Module, Store, schema, getUploadUrl, getFileUrl, deleteFile } from 'modelence/server';
import { ObjectId } from 'mongodb';

const dbDocuments = new Store('documents', {
  schema: {
    userId: schema.userId(),
    filePath: schema.string(),
    createdAt: schema.date(),
  },
});

export default new Module('documents', {
  stores: [dbDocuments],
  queries: {
    // Hand out a time-limited URL only for a document the caller owns.
    async getUrl({ documentId }, { user }) {
      if (!user) {
        throw new Error('Authentication required');
      }
      const doc = await dbDocuments.requireOne({
        _id: new ObjectId(documentId as string),
        userId: new ObjectId(user.id),
      });
      return getFileUrl(doc.filePath);
    },
  },
  mutations: {
    // Issue an upload URL for a path scoped to the caller, then record ownership.
    async requestUpload({ contentType }, { user }) {
      if (!user) {
        throw new Error('Authentication required');
      }
      const filePath = `private/users/${user.id}/${new ObjectId().toString()}`;
      const upload = await getUploadUrl({
        filePath,
        contentType: contentType as string,
        visibility: 'private',
      });
      await dbDocuments.insertOne({
        userId: new ObjectId(user.id),
        filePath,
        createdAt: new Date(),
      });
      return upload; // { url, fields, filePath } — the client POSTs the file to `url`
    },

    // Only the owner can delete.
    async remove({ documentId }, { user }) {
      if (!user) {
        throw new Error('Authentication required');
      }
      const doc = await dbDocuments.requireOne({
        _id: new ObjectId(documentId as string),
        userId: new ObjectId(user.id),
      });
      await deleteFile(doc.filePath);
      await dbDocuments.deleteOne({ _id: doc._id });
    },
  },
});
The client then calls your guarded methods — e.g. callMethod('documents.requestUpload', { contentType }) — instead of the file functions directly. Public assets that are safe for anyone to read can be served straight from their permanent public/... URL without a query.