Files
Pylo stores files as PyloMedia records, a built-in system entity that behaves like any other
entity. To put a file in your data model, connect its PyloMedia record to one of your entities
through a media relation, for example contact.avatar or project.attachments.
The upload flow
Uploads take two steps. Your client never talks to the storage backend, and no file bytes pass through your own server:
- Request an upload URL. The
createUploadmutation returns{ id, url }. Theidis the id of the PyloMedia record the upload will create. Theurlis a Pylo upload endpoint containing an embedded, expiring credential (valid for about 1 hour). - Send the bytes. POST the file to that
urlasmultipart/form-datawith a single file part. No auth headers needed: the credential in the URL is scoped to this one upload.
1
2
3
4
5
6
7
8
9
mutation {
createUpload(input: {
entity_relation_path: "Contact.avatar"
is_public: false
}) {
id # the future PyloMedia id
url # POST the file here
}
}Pylo reads the file's name, mime type, and size from the uploaded bytes, so you do not declare them in step 1. It writes the PyloMedia record only after step 2 finishes, so do not query the record in between. The SDKs wrap both steps in one call, see Uploading & Downloading.
Upload restrictions
entity_relation_path points at the media relation the file is destined for, in the form
"Entity.relation". Pylo enforces that relation's allowed mime types (wildcards like image/*
work) and file extensions during the upload. Omit input entirely and the upload runs
unrestricted and unattached; Pylo validates the file later, when you connect it to a relation.
Each upload URL takes one file, up to 2 GB.
Connecting files to entities
Connect an uploaded file like any other relation,
using the PyloMedia id in an upsert. Media relations are system-entity relations, so the suffixes
are _set (single), _add, and _remove (list). Pylo checks the relation's mime and extension
rules again at connect time, so a file uploaded without restrictions still cannot go onto a relation
it does not satisfy.
You can upload first and connect later, for example when a user picks a file before the record it belongs to exists. Keep the PyloMedia id around.
Reading files: download URLs
PyloMedia is a regular entity. Query it directly or through the relation on your entity:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
query {
contactById(id: "contact-uuid") {
data {
avatar {
data {
id
file_name
file_mime_type
file_size_bytes
file_extension
is_public
url # ready-to-use download URL, fresh on every query
}
}
}
}
}Pylo computes url at query time. For private files it is a signed, expiring URL (about 5
minutes by default, configurable per relation). Treat it as ephemeral: fetch it when you need it,
never store it. The createDownload(id) mutation creates a fresh URL for a PyloMedia id on demand.
Public vs. private files
By default every file is private: downloads require a freshly signed URL, which in turn requires an authenticated GraphQL request. Two ways to make a file publicly downloadable:
- Per file: pass
is_public: truewhen requesting the upload URL. The file gets a permanent public URL. - Per relation: configure the media relation with public downloads. Pylo then serves every file on that relation under a permanent URL, whatever the file's own flag says.
In both cases url returns the permanent public form instead of a signed one. Use public files for
assets you embed openly (logos, product images). Keep documents and user-generated content private.
Configuring media relations
A media relation is a normal entity relation whose target is PyloMedia. Per relation you can configure:
- Allowed mime types, for example
image/pngor wildcard patterns likeimage/* - Allowed file extensions, for example
pdf,docx - Public downloads: serve all connected files under permanent public URLs
- Download URL lifetime: how long signed URLs stay valid for private files