Guide

Files & images

Uploading, storing and serving files — and the ACL problem that catches everyone the first time.

Parse.File

A file is uploaded first and attached second. It has its own lifecycle and its own URL.

const file = new Parse.File('invoice.pdf', { base64 }, 'application/pdf');
await file.save({ useMasterKey: true });

document.set('attachment', file);
await document.save(null, { sessionToken });

file.url();   // where a client fetches it

Accepted sources: {base64}, {uri}, a byte array, and on parse-server 9.5+ a Buffer or stream for large uploads that should not be held in memory.

Declaring a file field

import {
  ParseField, implementACL, syncImageAcl, CloudFunction, catchError,
  BeforeSaveFile, AfterDeleteFile, AfterDelete,
} from 'parse-server-kit';

@ParseField({ type: 'File' })
declare attachment: any;

// Or a pointer to your own wrapper class, which is usually better -
// it gives you somewhere to put dimensions, alt text and a thumbnail.
@ParseField({ type: 'Pointer', targetClass: 'IMG' })
declare cover: any;
Why a wrapper class is usually worth it A bare Parse.File has a name and a URL and nothing else. A small IMG class — file, width, height, alt text, blurhash, a generated thumbnail — gives you a row you can attach an ACL to, run a trigger on, and reuse from several parents. It is also what makes the ACL cascade below possible.

The problem everyone hits

A file's URL is not protected by the parent record's ACL. Anyone holding the URL can fetch the bytes. What the ACL protects is the row that points at it.

A file URL is effectively a bearer token Hiding the parent record hides the reference, not the file. For genuinely private documents, keep them out of public storage and serve them through an endpoint that checks permissions, or use parse-server 9.8+'s fileDownload restriction.

Making images follow their parent

When you use a wrapper class, its row has its own ACL — and that ACL does not update itself when the parent's visibility changes. Publish an article and its cover stays invisible; archive it and the cover stays readable.

// After the parent has its FINAL ACL, before saving it.
article.setACL(implementACL({
  roleRules: [{ role: 'Admin', read: true, write: true }],
  owner: [{ user: authorId, read: true, write: true }],
  publicRead: status === 'published',
}));

syncImageAcl(article, ['cover', 'gallery']);

await article.save(null, { sessionToken });

syncImageAcl copies the parent's ACL onto each pointed-to row — single pointer or array — and marks them dirty so the parent's save cascades the change. An ACL-only update does not re-process the file, so it is cheap.

Call it at every point the parent's ACL is set: create, approve, publish, archive, restore. A cover image that is still public after its article was withdrawn is almost always a transition that forgot this line.

Where visibility is derived inside @BeforeSave, pass that publicRead into implementACL at the call site — syncImageAcl runs before the trigger, so it would copy an ACL that is one step out of date.

An upload endpoint

@CloudFunction({
  methods: ['POST'],
  validation: { requireUser: true, fields: {
    base64:   { required: true },
    fileName: { required: true },
  } },
})
static async uploadImage(req: Parse.Cloud.FunctionRequest) {
  const user = req.user!;

  const file = new Parse.File(req.params.fileName, {
    base64: req.params.base64,
  });
  const [fileErr] = await catchError(file.save({ useMasterKey: true }));
  if (fileErr) throw fileErr;

  const img = new IMG();
  img.set('file', file);
  img.set('alt', req.params.alt ?? '');
  img.setACL(implementACL({
    owner: [{ user: user.id, read: true, write: true }],
    roleRules: [{ role: 'Admin', read: true, write: true }],
  }));

  const [err, saved] = await catchError(
    img.save(null, { sessionToken: user.getSessionToken() })
  );
  if (err) throw err;
  return saved;
}
fromParams skips file pointers on purpose BaseModel.fromParams() ignores Pointer fields whose target is in excludedPointerClassesIMG and File by default. An uploaded file needs saving and processing before it can be attached, so building a bare pointer from request params would produce a reference to nothing. Handle files explicitly, as above. The list is configurable through configureKit.

File triggers

@BeforeSaveFile()
static async onBeforeSaveFile(req: any) {
  const { file, user } = req;
  if (!user) throw new Parse.Error(119, 'Sign in to upload');

  // Resize, strip EXIF, scan, or reject by type or size here.
  return file;
}

@AfterDeleteFile()
static async onAfterDeleteFile(req: any) {
  // Clean up derived thumbnails.
}

File triggers pass the class rather than a name — parse-server removed the older beforeSaveFile() style methods, and this library handles that translation, so the decorators work unchanged across versions.

Deleting

Deleting a row that points at a file does not delete the file. Storage keeps it until something removes it.

@AfterDelete()
static async onAfterDelete(req: Parse.Cloud.AfterDeleteRequest<IMG>) {
  const file = req.object.get('file') as Parse.File | undefined;
  if (file) await catchError(file.destroy({ useMasterKey: true }));
}

Put it on the wrapper class and every parent that removes an image gets the cleanup for free.

Where files actually live

AdapterSuits
Filesystem (default)Development. Lost on redeploy, not shared between instances
S3, Google Cloud StorageProduction
GridFSKeeping files in MongoDB — simple, but grows the database

Configure it on Parse Server, not here. Note that /files is allowed through restrictRoutes without authentication, so anything reachable by URL is public unless the storage layer says otherwise.

Common mistakes

SymptomCause
An image is invisible while its record is publicsyncImageAcl not called after the parent's ACL was set
An image stays visible after the record was hiddenSame, on the transition rather than on create
The file pointer is empty after fromParamsExpected — file classes are excluded by design
Files vanish on deployFilesystem adapter in production
Storage grows and never shrinksRows deleted without destroying their files
A private document is reachable by URLFile URLs are not ACL-protected — serve them through an endpoint

For adapter configuration and the file API itself, see the Parse files guide.