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;
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.
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;
}
BaseModel.fromParams() ignores Pointer fields whose target is in
excludedPointerClasses — IMG 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
| Adapter | Suits |
|---|---|
| Filesystem (default) | Development. Lost on redeploy, not shared between instances |
| S3, Google Cloud Storage | Production |
| GridFS | Keeping 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
| Symptom | Cause |
|---|---|
| An image is invisible while its record is public | syncImageAcl not called after the parent's ACL was set |
| An image stays visible after the record was hidden | Same, on the transition rather than on create |
The file pointer is empty after fromParams | Expected — file classes are excluded by design |
| Files vanish on deploy | Filesystem adapter in production |
| Storage grows and never shrinks | Rows deleted without destroying their files |
| A private document is reachable by URL | File URLs are not ACL-protected — serve them through an endpoint |
For adapter configuration and the file API itself, see the Parse files guide.