File attachments
Xata provides three different ways to upload files to a file column within a database.
- Upload URLs, when requested from an existing file object, provide a temporary, secure URL to upload files to. Because they don't require an API key they can be used client-side and are good when dealing with large files through a client interface like a browser. This method comes at the downside of requiring extra steps across the client and server side.
- Record APIs provide the easiest way to upload files, and are the only methods that allow uploading files during the same step as creating a record. The downside is that they only support file payloads under 20MB and will likely run over the limits of serverless functions from providers like Vercel which can be much lower.
- Binary APIs allow for uploading large files on an existing record. Unfortunately, unlike Upload URLs, they must be called server side, which will likely run over the limits of serverless functions from providers like Vercel. This makes their usefulness limited to scenarios where you control the environment completely, like uploading large files from a local machine.
Xata recommends using Upload URLs for most scenarios. Record and Binary APIs exist for more specific situations where you know the size of your data or have complete control over the hosting infrastructure.
Xata provides a secure method to gain temporary upload URLs to upload or update files on existing records. They are the recommended pattern for uploading large files through a web browser or when you want to transfer a file client side. Upload URLs allow you to bypass the size restrictions of many server-side function hosts like Vercel and send files directly to Xata within its larger REST API limits.
The only negative to using upload URLs versus the file or binary APIs is that this method requires multiple steps (creating, then updating the record), usually across both the server and the client.
A typical pattern for a JavaScript application follows:
- On the server side create a record with an empty file using
xata.db.myTableName.create()
, simultaneously requesting anuploadUrl
from that newly created, but empty file. - On the server, pass a response with the file's
uploadUrl
to the client. - On the client, upload the file using a
PUT
request against theuploadUrl
given, making sure to account for any cleanup necessary if the upload fails.
A server-side API route (heavily simplified, and not accounting for your particular framework) might look like this:
// ... Your api route code
// Create an empty record with no base64 content on a `myTableName` table. The column for the file is `myFileColumnName`.
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 record, which has the `uploadUrl`.
return Response(record.toSerializable());
On the client side, you would typically have a handleSubmit
function that calls the API created above.
const handleSubmit = async (e) => {
// Grab the form data
const formData = new FormData();
const fileObj = file;
formData.append('fileType', fileObj.type);
try {
// This route creates new image and tag records in Xata
// If you look at the api route code, you'll see that we're not actually
// uploading the image here. Instead, we're creating a record in the database
// with a temporary, empty image. We do this because we need to generate a
// pre-signed URL for the image upload.
const response = await fetch('/api/create-record', {
method: 'POST',
body: formData
});
if (response.status !== 200) {
throw new Error("Couldn't create image record");
}
const record = await response.json();
// The response include a pre-signed uploadUrl on the record. Below, we then send a file
// directly to Xata on the client side using the PUT request. This lets us upload
// large files that would otherwise exceed the limit for serverless functions on
// services like Vercel.
if (response.status === 200) {
try {
await fetch(record.myFileColumnName.uploadUrl, { method: 'PUT', body: file });
} catch (error) {
// Delete the record (using a separate API route)
await fetch(`/api/delete-record/${record.id}`, { method: 'DELETE' });
throw new Error("Couldn't upload image because the image wasn't accepted");
}
} else {
throw new Error("Couldn't upload image because the record wasn't created");
}
} catch (error) {
throw new Error("Couldn't upload image because the record wasn't created");
}
};
A full example of the above pattern in a Next.js application is available in our Gallery app repo on GitHub.
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'
}
});
record = xata.records().insert("Users", {
"name": "Keanu",
"photo": {
"name": "file.png",
"mediaType": "image/png",
"base64Content": "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEklEQVR42mNk+M9QzwAEjDAGACCDAv8cI7IoAAAAAElFTkSuQmCC",
}
})
recordsClient, _ := xata.NewRecordsClient()
record, _ := recordsClient.Insert(context.TODO(), xata.InsertRecordRequest{
RecordRequest: xata.RecordRequest{
TableName: "Users",
},
Body: map[string]*xata.DataInputRecordValue{
"name": xata.ValueFromString("Keanu"),
"photo": xata.ValueFromInputFile(xata.InputFile{
Name: "file.png",
MediaType: xata.String("image/png"),
Base64Content: xata.String("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEklEQVR42mNk+M9QzwAEjDAGACCDAv8cI7IoAAAAAElFTkSuQmCC"),
}),
},
})
// POST https://{workspace}.{region}.xata.sh/db/{db}:{branch}/tables/{table}/data
{
"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'
}
});
record = xata.records().update("Users", "record_id", {
"photo": {
"base64Content": "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEklEQVR42mNk+M9QzwAEjDAGACCDAv8cI7IoAAAAAElFTkSuQmCC"
}
})
recordsClient, _ := xata.NewRecordsClient()
record, _ := recordsClient.Update(context.TODO(), xata.UpdateRecordRequest{
RecordRequest: xata.RecordRequest{
TableName: "Users",
},
RecordID: "record_id",
Body: map[string]*xata.DataInputRecordValue{
"photo": xata.ValueFromInputFile(xata.InputFile{
Base64Content: xata.String("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEklEQVR42mNk+M9QzwAEjDAGACCDAv8cI7IoAAAAAElFTkSuQmCC"),
}),
},
})
// PATCH https://{workspace}.{region}.xata.sh/db/{db}:{branch}/tables/{table}/data/record_id
{
"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'
}
]
});
record = xata.records().update("Users", "record_id", {
"photos": [
{
"id": "existing_file_id"
},
{
"id": "new_id",
"base64Content":
"iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEklEQVR42mNk+M9QzwAEjDAGACCDAv8cI7IoAAAAAElFTkSuQmCC"
}
]
})
recordsClient, _ := xata.NewRecordsClient()
record, _ := recordsClient.Update(context.TODO(), xata.UpdateRecordRequest{
RecordRequest: xata.RecordRequest{
TableName: "Users",
},
RecordID: "record_id",
Body: map[string]*xata.DataInputRecordValue{
"photos": xata.ValueFromInputFileArray(xata.InputFileArray{
{
Id: xata.String("existing_file_id"),
},
{
Name: xata.String("new_id"),
Base64Content: xata.String("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEklEQVR42mNk+M9QzwAEjDAGACCDAv8cI7IoAAAAAElFTkSuQmCC"),
},
}),
},
})
// PATCH https://{workspace}.{region}.xata.sh/db/{db}:{branch}/tables/{table}/data/record_id
{
"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']);
user = xata.records().get("Users", "record_id", columns=["photo.name", "photo.base64Content"])
recordsClient, _ := xata.NewRecordsClient()
user, _ := recordsClient.Get(context.Background(), xata.GetRecordRequest{
RecordRequest: xata.RecordRequest{
TableName: "Users",
},
RecordID: "record_id",
Columns: []string{"photo.name", "photo.base64Content"},
})
curl 'https://{workspace}.{region}.xata.sh/db/{db}:{branch}/tables/{table}/data/{record_id}?columns=photo.name,photo.base64Content' \
--header 'authorization: Bearer xau_REDACTED'
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 });
record = xata.records().update("Users", "record_id", {"photo": None})
// PATCH https://{workspace}.{region}.xata.sh/db/{db}:{branch}/tables/{table}/data/record_id
{
"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'} ] });
record = xata.records().update("Users", "record_id", {
"photos": [
{"id": "id_to_keep_1"},
{"id": "id_to_keep_2"}
]
})
// PATCH https://{workspace}.{region}.xata.sh/db/{db}:{branch}/tables/{table}/data/record_id
{
"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();
photos = xata.data().query("Users", {
"columns": ["name", "photo.url", "photo.size"],
"filter": { "photo.mediaType": "image/png" },
"sort": { "photo.size": "desc" }
})
searchClient, _ := xata.NewSearchAndFilterClient()
records, _ := searchClient.Query(context.TODO(), xata.QueryTableRequest{
TableName: "Users",
Payload: xata.QueryTableRequestPayload{
Columns: []string{"name", "photo.url", "photo.size"},
// Nested filters currently are not support yet in xata-go
Sort: xata.NewSortExpressionFromStringSortOrderMap(map[string]xata.SortOrder{
"photo.size": xata.SortOrderDesc,
}),
},
})
// POST https://{workspace}.{region}.xata.sh/db/{db}:{branch}/tables/{table}/query
{
"columns": ["name", "photo.url", "photo.size"],
"filter": { "photo.mediaType": "image/png" },
"sort": { "photo.size": "desc" }
}
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();
photos = xata.data().query("Users", {
"columns": ["name", "photo.base64Content", "photo.signedUrl", "photo.*"],
"filter": { "photo.mediaType": "image/png" },
"sort": { "photo.size": "desc" }
})
searchClient, _ := xata.NewSearchAndFilterClient()
records, _ := searchClient.Query(context.TODO(), xata.QueryTableRequest{
TableName: "Users",
Payload: xata.QueryTableRequestPayload{
Columns: []string{"name", "photo.base64Content", "photo.signedUrl", "photo.*"},
// Nested filters are currently not supported in xata-go
Sort: xata.NewSortExpressionFromStringSortOrderMap(map[string]xata.SortOrder{
"photo.size": xata.SortOrderDesc,
}),
},
})
// POST https://{workspace}.{region}.xata.sh/db/{db}:{branch}/tables/{table}/query
{
"columns": ["name", "photo.base64Content", "photo.signedUrl", "photo.*"],
"filter": { "photo.mediaType": "image/png" },
"sort": { "photo.size": "desc" }
}
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);
# file_content = bytes
response = xata.files().put("table_name", "record_id", "column_name", file_content)
filesClient, _ := xata.NewFilesClient()
file, _ := filesClient.Put(context.TODO(), xata.PutFileRequest{
TableName: "table_name",
RecordID: "record_id",
ColumnName: "column_name",
Data: []byte(`data`), // file content
})
curl --request PUT 'https://{workspace}.{region}.xata.sh/db/{db}:{branch}/tables/{table}/data/{record_id}/column/{column_name}/file' \
--header 'Content-Type: image/jpeg' \
--header 'Authorization: Bearer xau_REDACTED' \
--data-binary '@/path/to/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);
# file_content = bytes
response = xata.files().put_item("table_name", "record_id", "column_name", "file_id", file_content)
filesClient, _ := xata.NewFilesClient()
file, _ := filesClient.PutItem(context.TODO(), xata.PutFileItemRequest{
TableName: "table_name",
RecordID: "record_id",
ColumnName: "column_name",
FileID: "file_id",
Data: []byte(`data`), // file content
})
curl --request PUT 'https://{workspace}.{region}.xata.sh/db/{db}:{branch}/tables/{table}/data/{record_id}/column/{column_name}/file/{file_id}' \
--header 'Content-Type: image/jpeg' \
--header 'Authorization: Bearer xau_REDACTED' \
--data-binary '@/path/to/file'
file
column type:
const file = await xata.files.download({ table: 'table_name', column: 'column_name', record: 'record_id' });
file = xata.files().get("table_name", "record_id", "column_name")
filesClient, _ := xata.NewFilesClient()
file, _ := filesClient.Get(context.TODO(), xata.GetFileRequest{
TableName: "table_name",
RecordID: "record_id",
ColumnName: "column_name",
})
curl 'https://{workspace}.{region}.xata.sh/db/{db}:{branch}/tables/{table}/data/{record_id}/column/{column_name}/file' \
--header 'Authorization: Bearer xau_REDACTED' \
--output download.jpeg
file[]
(file array) column type:
const file = await xata.files.download({
table: 'table_name',
column: 'column_name',
record: 'record_id',
fileId: 'file_id'
});
file = xata.files().get_item("table_name", "record_id", "column_name", "file_id")
filesClient, _ := xata.NewFilesClient()
file, _ := filesClient.GetItem(context.TODO(), xata.GetFileItemRequest{
TableName: "table_name",
RecordID: "record_id",
ColumnName: "column_name",
FileID: "file_id",
})
curl 'https://{workspace}.{region}.xata.sh/db/{db}:{branch}/tables/{table}/data/{record_id}/column/{column_name}/file/{file_id}' \
--header 'Authorization: Bearer xau_REDACTED' \
--output download.jpeg
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' });
response = xata.files().delete_item("table_name", "record_id", "column_name", "file_id")
filesClient, _ := xata.NewFilesClient()
file, _ := filesClient.DeleteItem(context.TODO(), xata.DeleteFileItemRequest{
TableName: "table_name",
RecordID: "record_id",
ColumnName: "column_name",
FileID: "file_id",
})
curl --request DELETE 'https://{workspace}.{region}.xata.sh/db/{db}:{branch}/tables/{table}/data/{record_id}/column/{column_name}/file/{file_id}' \
--header 'Authorization: Bearer xau_REDACTED' \