What is Xata?
File attachments

File attachments

Edit on GitHub

Xata offers general purpose file attachment capabilities. In order to simplify the management of large binary objects and to improve the developer experience, Xata integrates files support directly into the database itself. The aim is to offer a unified, seamless experience by exposing file support under one API, security model, and SDK. Using this approach, the queries, filters, summaries, aggregations from the SDK can also be used on the files' metadata. The metadata (file name, media type, tags) is also indexed for search in the optional Search store so it can be included in search results.

Add the file column type to start working with files
Add the file column type to start working with files

Xata provides three different ways to interact with file attachments within a file column in a database.

Upload URLs (for uploads)

  • ✅ Client side (doesn't require using an API key, and can be run in the browser).
  • ✅ Upload large files.
  • ⚠️ Require extra steps across the client and server side.
  • Used only for initial upload or replacement of the actual files.

Record APIs (for deleting, downloading, and querying files)

  • ✅ Upload files in the same step as creating a record.
  • ✅ Useful for querying and modifying the metadata around existing files.
  • ⚠️ Only supports files up to 20MB (total per request).
  • ⚠️ Server side only.
  • ⚠️ Likely exceed limits of serverless functions from providers like Vercel which can be much lower.

Binary APIs (for controlled environments)

  • ✅ Upload files to existing record.
  • ✅ Upload large files.
  • ⚠️ Server side only.
  • ⚠️ Likely exceed limits of serverless functions from providers like Vercel which can be much lower.
  • Good for when you own the environment completely, like uploading large files from a local machine.

#

Upload files using upload URLs

Upload URLs are a temporary, secure URL that allows uploading files to Xata from the client side. They are the recommended pattern for uploading large files from a client e.g. a web browser. Upload URLs have large limits and allow you to bypass the size restrictions of many server-side function hosts like Vercel.

The downside to using upload URLs is they require multiple steps: creating, then updating the record.

On the server side use xata.db.myTableName.create to create (or update) a record with an empty file and simultaneously request an uploadUrl.

A server-side API route (heavily simplified, and not accounting for your particular framework) might look like this:

app/api/create-record.ts
// ... Your api route code

// Create an empty record with no base64 content on a `myTableName` table. The column for the file is `myFileColumnName`.
// Can also use `.update` for existing records
const record = await xata.db.myTableName.create(
  { name, myFileColumnName: { name: name, mediaType: 'image/png', base64Content: '' } },
  // Request an uploadUrl from the created record. We'll use this client-side to update the record.
  ['myFileColumnName.uploadUrl']
);

// Return the `uploadUrl` in the API response.
return Response({ myUploadUrl: record.myFileColumnName.uploadUrl });

Upload the file using a PUT request to the uploadUrl returned from the server side.

app/upload.tsx
try {
  // Call the server API from step 1 above
  const response = await fetch('/api/create-record', {
    method: 'POST',
    body: formData
  });
  if (response.status !== 200) {
    throw new Error("Couldn't create record");
  }

  const responseJson = await response.json();

  try {
    // Put the file inside a FormData object
    const formData = new FormData();
    const fileObj = file;
    formData.append('fileType', fileObj.type);
    // Use `myUploadUrl` from the server response to upload the file on the client
    await fetch(responseJson.myUploadUrl, { method: 'PUT', body: file });
  } catch (error) {
    throw new Error("Couldn't upload image because the image wasn't accepted");
  }
} catch (error) {
  throw new Error("Couldn't upload image because the record wasn't created");
}

A minimal image upload example using Next.js server components is available as a sample application on GitHub. A more advanced example is available in our Gallery app.

All Xata record APIs can be used to create, read, update, delete and query files.

const record = await xata.db.Users.create({
  name: 'Keanu',
  photo: {
    name: 'file.png',
    mediaType: 'image/png',
    base64Content:
      'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEklEQVR42mNk+M9QzwAEjDAGACCDAv8cI7IoAAAAAElFTkSuQmCC'
  }
});
const user = await xata.db.Users.update('record_id', {
  photo: {
    base64Content:
      'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEklEQVR42mNk+M9QzwAEjDAGACCDAv8cI7IoAAAAAElFTkSuQmCC'
  }
});

// or, using the `update` method on the record object:

user.update({
  photo: {
    base64Content:
      'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEklEQVR42mNk+M9QzwAEjDAGACCDAv8cI7IoAAAAAElFTkSuQmCC'
  }
});

In the following example the photos column is of type file[] (file array). The existing file ids from the array must be present in the update.

const user = await xata.db.Users.update('record_id', {
  photos: [
    {
      id: 'existing_file_id'
    },
    {
      id: 'new_id',
      base64Content:
        'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEklEQVR42mNk+M9QzwAEjDAGACCDAv8cI7IoAAAAAElFTkSuQmCC'
    }
  ]
});

// or, using the `update` method on the record object:

user.update({
  photos: [
    {
      id: 'existing_file_id'
    },
    {
      id: 'new_id',
      base64Content:
        'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEklEQVR42mNk+M9QzwAEjDAGACCDAv8cI7IoAAAAAElFTkSuQmCC'
    }
  ]
});

The base64Content must be requested explicitly, it is not returned when selecting columns using wildcard.

const user = await xata.db.Users.read('record_id', ['photo.name', 'photo.base64Content']);

Response:

{
  "id": "record_id",
  "photo": {
    "name": "file.png",
    "base64Content": "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEklEQVR42mNk+M9QzwAEjDAGACCDAv8cI7IoAAAAAElFTkSuQmCC"
  }
}
const user = await xata.db.Users.update('record_id', { photo: null });

// or, using the `update` method on the record object:

await user.update({ photo: null });

A file array item is deleted by setting the array to the set of ids that need to be kept.

const user = await xata.db.Users.update('record_id', { photos: [{id: 'id_to_keep_1'}, {id: 'id_to_keep_2'} ] });

// or, using the `update` method on the record object:

await user.update({ photos: [{id: 'id_to_keep_1'}, {id: 'id_to_keep_2'} ] });

Here is an example of retrieving access URLs for all files from the photo column, that are image/png, sorted by file size:

const photos = await xata.db.Users.select(['name', 'photo.url', 'photo.size'])
  .filter({ 'photo.mediaType': 'image/png' })
  .sort('photo.size', 'desc')
  .getMany();

Fields base64Content and signedUrl within the photo object are retrieved only when they are explicitly specified in the request. If you use a wildcard to request all fields of the photo object, these specific fields will not be included automatically. You must list them individually to retrieve their values.

const photos = await xata.db.Users.select(['name', 'photo.base64Content', 'photo.signedUrl', 'photo.*'])
  .filter({ 'photo.mediaType': 'image/png' })
  .sort('photo.size', 'desc')
  .getMany();

File attachment metadata is also available by default in search results, if the optional Search store is enabled. All metadata fields including the url,signedUrl and uploadUrl are returned as part of matching documents, but not the file content. Note that the url value is accessible only when enablePublicUrl is set to true in the file's configuration.

const results = await xata.search.all('Keanu', {
  tables: ['Users']
});
Example Search response
{
  "totalCount": 1,
  "records": [
    {
      "table": "Users",
      "record": {
        "id": "rec_cn2baqiit8nelj83jag0",
        "name": "Keanu Reeves",
        "photo": {
          "name": "img.jpg",
          "mediaType": "image/jpeg",
          "enablePublicUrl": true,
          "signedUrlTimeout": 600,
          "uploadUrlTimeout": 86400,
          "size": 1,
          "version": 0,
          "url": "https://us-east-1.storage.xata.sh/f13f4im2411tpd9njp32mgt9fc",
          "signedUrl": "https://us-east-1.xata.sh/file/...",
          "uploadUrl": "https://q867qv.us-east-1.upload.xata.sh/file/...",
          "attributes": {
            "height": 3205,
            "width": 2561
          }
        },
        "xata": {
          "createdAt": "2024-02-08T11:05:14.344Z",
          "highlight": {
            "name": [
              "<em>Keanu</em> Reeves"
            ]
          },
          "score": 0.2876821,
          "table": "Users",
          "updatedAt": "2024-02-08T12:03:57.044Z",
          "version": 1
        }
      }
    }
  ]
}

Since all record APIs use JSON for both request and response body, the file content needs to be encoded. For reasons like performance or data size on the wire, encoding the content might not be desired. To work directly with binary file content, Xata introduces new file APIs. Similar to the other Xata APIs, the file APIs require the Authorization header and a valid API key.

file column type:

await xata.files.upload({ table: 'table_name', column: 'column_name', record: 'record_id' }, file);

Column type is file[] (file array). The fileId is optional and a unique id will be automatically generated if not provided.

await xata.files.upload({ table: 'table_name', column: 'column_name', record: 'record_id', fileId: 'id' }, file);

Use the update record API to set file metadata such as name and mediaType for files uploaded using the binary file API.

file column type:

const file = await xata.files.download({ table: 'table_name', column: 'column_name', record: 'record_id' });

file[] (file array) column type:

const file = await xata.files.download({
  table: 'table_name',
  column: 'column_name',
  record: 'record_id',
  fileId: 'file_id'
});

Column type is file[] (file array). fileId is required to identify the array item to be deleted.

await xata.files.delete({ table: 'table_name', column: 'column_name', record: 'record_id', fileId: 'id' });

Xata provides three ways to expose a file's URL to any request. This allows you to build a range of products from public facing websites, to more security-minded applications that need to think through authentication. Files are secure by default, with action required through these methods to provide access.

A file's access can be toggled in the app
A file's access can be toggled in the app

Authenticated URLs are private URLs that can be used to access a file with a valid Xata API key. They are available in the url field of a file object if the file is not configured for public access.

These URLs are especially useful if you need quick, high-throughput, and concurrent download access, with cached data.

// Disallow this User photo to be accessed publicly at any time by the URL alone
const user = await xata.db.Users.update('record_id', {
  photo: {
    enablePublicUrl: false
  }
});

// Retrieve the private URL on the file in the "photo" column
const { url } = user.photo.transform({ quality: 50 });

A Xata file can be configured for public access, resulting in a URL that is publicly available without the need of an API key or a signature. A public URL does not expire and offers access until the file is reconfigured to remove public access. To enable public access for a file, you can set the enablePublicUrl field to true in the file's configuration. This is particularly useful for public websites and for sharing public content.

You can use the public URL directly without writing code. For instance:

https://us-east-1.storage.xata.sh/4u1fh2o6p10blbutjnphcste94 is available publicly.

// Allow this user's photo to be accessed publicly at any time by the URL alone
const user = await xata.db.Users.update('record_id', {
  photo: {
    enablePublicUrl: true
  }
});

// Retrieve the public URL on the file in the "photo" column
const { url } = user.photo.transform({ quality: 50 });

Within the Xata UI you can make files public by default for the column by setting Make files public by default when creating the column. This can also be done programtically by setting a parameter on the file column in the schema.

Entire columns can be made public by default
Entire columns can be made public by default

A signed URL offers authenticated access to a file without requiring an API key. The URL contains the key (signature) within it, so anyone holding the URL can get access to the file. Because of their on demand nature, signed URLs need to be requested directly and do not come along with the default request on the record.

// Returns the signed URL for records[0].photo.signedUrl
const records = await xata.db.Users.select(['id', 'name', 'photo.*', 'photo.signedUrl']).getMany();

// Returns an empty string for records[0].photo.signedUrl
const records = await xata.db.Users.getMany();

Signed URLs have a configurable time to live (TTL), which specifies when access to the URL expires. To avoid permanent public access, the TTL can be set to a maximum of 24h. You can modify the timeout duration of the signed URL for each file by adjusting the signedUrlTimeout field within the file object. Please note that signedUrlTimeout expects a positive number, defining the timeout duration in seconds. The default value is set to 60 seconds.

Use signed URLs when you need temporary access without revealing the API key, such as rendering an image without disclosing a permanent image URL.

// Set the timeout to 10 minutes for this user's photo
const user = await xata.db.Users.update('record_id', {
  photo: {
    signedUrlTimeout: 600 // In seconds
  }
});

// Retrieve the signed URL on the file in the "photo" column
const { signedUrl } = user.photo.transform({ quality: 50 });

An upload URL provides a secure and authenticated method to upload or update a file without requiring an API key. This URL includes a time-limited signature, that ensures that only users with the URL can upload a file within a set time frame.

The URLs are similar to signed URLs in that they must be specifically requested and are not automatically included with a standard record request. This improves security by controlling access and limiting the window during which uploads can occur.

HTTP PUT is the only valid method for a request using the upload URL.

An upload URL is assigned to a specific file. Subsequent uploads using the same URL will overwrite the existing file content, instead of generating additional files. Every time the URL is used, it updates the same specific file it's linked to.

// Returns the upload URL for records[0].photo.uploadUrl
const records = await xata.db.Users.select(['id', 'name', 'photo.*', 'photo.uploadUrl']).getMany();

// Returns an empty string for records[0].photo.uploadUrl
const records = await xata.db.Users.getMany();

Upload URLs have a configurable time to live (TTL), which specifies when access to the URL expires. The default is 24h. You can modify the timeout duration of the upload URL for each file by adjusting the uploadUrlTimeout field within the file object. The value represents a duration in seconds.

Use upload URLs when you need temporary write access without revealing the API key, such as exposing uploads in a web application.

// Set the timeout to 10 minutes for the photo upload URL
const user = await xata.db.Users.update('record_id', {
  photo: {
    uploadUrlTimeout: 600 // Time in seconds
  }
});

The SDK documentation for file attachments contains a more detailed pattern for using an uploadUrl in a typical web scenario. The sample gallery app repo on Github also documents a complete example of the pattern for Next.js.

File attachments are delivered through an integrated content delivery network (CDN) which minimizes access latency by using regional caches.

Caching is enabled for all signed and public URLs. This is to avoid caching data that is secured by API key permissions. This feature is enabled by default and doesn't require any configuration. Content from publicly accessible and signed URLs is cached for a duration of 4 hours.

As with all cache systems, the fundamental problem is cache invalidation - maintaining cache access performance while ensuring updated content and preventing outdated entries. In its architectural approach, Xata addresses the issue of cache invalidation by treating file content as dynamic - analogous to data within a database.

Consequently, it's recommended that clients dynamically obtain access URLs from the database, rather than persisting them in static resources. When the target file is modified, URLs can become invalid, resulting in access disruptions if stored externally to the database.

Through the process of generating a new URL after each update, the system prevents the delivery of outdated and stale content, regardless of any caches or proxies between the client and storage service. For the client application, this translates to no need for cache time-to-live (TTL) or waiting for cache invalidation. When the URL is fetched directly from the database, it is guaranteed to retrieve the most recent version of the file available.

On this page

Upload files using upload URLs1. Create/Update a record with an empty file (server side)2. Upload the file (client side)Record APIsUpload a file through inserting a new recordUpdate a file through updating a recordAppend a file to an array through updating a recordDownload a file through reading a recordDelete a file through updating a recordDelete a file from an array through updating a recordQuery files metadataFile attachments within search resultsFile (binary) APIsUpload a file using file APIsAppend a file to an array using file APIsDownload a file using file APIsDelete a file from an array using file APIsFile access and securityAuthenticated URLsPublic URLsSigned URLsUpload URLsContent delivery network (CDN)