> How file uploads work in Pylo, PyloMedia, upload URLs, download URLs, public vs private

# 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:

1. **Request an upload URL.** The `createUpload` mutation returns `{ id, url }`. The `id` is the id
   of the PyloMedia record the upload will create. The `url` is a Pylo upload endpoint containing an
   embedded, expiring credential (valid for about 1 hour).
2. **Send the bytes.** POST the file to that `url` as `multipart/form-data` with a single file part.
   No auth headers needed: the credential in the URL is scoped to this one upload.

```graphql filename="createUpload.graphql"
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](/files/sdk).

> **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](/concepts/entities-and-relations#relations),
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:

```graphql filename="download.graphql"
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: true` when 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/png` or wildcard patterns like `image/*`
- **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

Next: [Uploading & Downloading](/files/sdk)
