Note
Attachment helpers are currently in an alpha state, intended strictly for testing. Expect breaking changes and instability as development continues.
Do not rely on this package for production use.
React Native storage and transport adapters for PowerSync attachments.
This package provides:
- Local storage adapters (
LocalStorageAdapter) — persist attachment files on device. - Streaming transport adapters (
AttachmentTransportAdapter) — move attachment bytes directly between the local file and remote storage using native APIs, without buffering the whole file in JS memory. Recommended for large files (recordings, videos) on lower-end devices.
npm install @powersync/attachments-storage-react-native
# or
pnpm add @powersync/attachments-storage-react-native
# or
yarn add @powersync/attachments-storage-react-nativeYou'll also need to install one of the supported file system libraries. The same library powers both the local storage adapter and the native transport adapter for that platform.
Important
Requires Expo 54+
npx expo install expo-file-systemnpm install @dr.pogodin/react-native-fsThe local storage adapter handles file persistence on the device. Pass it to the queue as localStorage.
import { ExpoFileSystemStorageAdapter } from '@powersync/attachments-storage-react-native';
import { AttachmentQueue } from '@powersync/react-native';
const localStorage = new ExpoFileSystemStorageAdapter();
const attachmentQueue = new AttachmentQueue({
db,
localStorage,
remoteStorage, // your RemoteStorageAdapter (buffered upload/download/delete)
watchAttachments
});import { ReactNativeFileSystemStorageAdapter } from '@powersync/attachments-storage-react-native';
import { AttachmentQueue } from '@powersync/react-native';
const localStorage = new ReactNativeFileSystemStorageAdapter();
const attachmentQueue = new AttachmentQueue({
db,
localStorage,
remoteStorage,
watchAttachments
});Both local adapters accept an optional storageDirectory parameter:
const localStorage = new ExpoFileSystemStorageAdapter('/custom/path/to/attachments/');A transport adapter owns all remote operations (upload / download / delete) and transfers bytes natively — the file never enters the JS heap. Implement remote delete via the deleteFile callback.
The queue takes exactly one remote mechanism: either remoteStorage or transportAdapter. A transportAdapter replaces remoteStorage entirely, so none is needed alongside it.
Both transports are backend-agnostic: you supply resolver callbacks that map an attachment to a request (typically a presigned URL from your backend).
Note
The Expo streaming transport requires Expo SDK 56+ (expo-file-system >=56). The ExpoFileSystemStorageAdapter itself still works on Expo SDK 54+.
import { ExpoFileSystemStorageAdapter } from '@powersync/attachments-storage-react-native';
import { AttachmentQueue } from '@powersync/react-native';
const localStorage = new ExpoFileSystemStorageAdapter();
const transportAdapter = localStorage.createTransportAdapter({
resolveUpload: async (attachment) => ({
url: await getSignedUploadUrl(attachment.filename), // from your backend
httpMethod: 'PUT',
mimeType: attachment.mediaType ?? 'application/octet-stream'
}),
resolveDownload: async (attachment) => ({
url: await getSignedDownloadUrl(attachment.filename)
}),
deleteFile: async (attachment) => {
await deleteFromRemoteStorage(attachment.filename); // your SDK / DELETE call
}
});
const attachmentQueue = new AttachmentQueue({
db,
localStorage,
transportAdapter, // owns upload/download/delete — no remoteStorage needed
watchAttachments
});Identical options shape. The upload is sent as a raw binary PUT (binaryStreamOnly), suitable for presigned S3/Supabase URLs.
import { ReactNativeFileSystemStorageAdapter } from '@powersync/attachments-storage-react-native';
const localStorage = new ReactNativeFileSystemStorageAdapter();
const transportAdapter = localStorage.createTransportAdapter({
resolveUpload: async (attachment) => ({
url: await getSignedUploadUrl(attachment.filename),
httpMethod: 'PUT',
mimeType: attachment.mediaType ?? 'application/octet-stream'
}),
resolveDownload: async (attachment) => ({
url: await getSignedDownloadUrl(attachment.filename)
}),
deleteFile: async (attachment) => {
await deleteFromRemoteStorage(attachment.filename);
}
});For files already written to disk (recordings, camera/picker output), use AttachmentQueue.saveFileFromUri to register them without reading the bytes into memory — the local adapter's moveFile relocates the file into managed storage.
await attachmentQueue.saveFileFromUri({
localUri, // path to the existing file
fileExtension: 'm4a',
mediaType: 'audio/m4a'
});Implement the LocalStorageAdapter interface from @powersync/common:
initialize()- Create the storage directory if it doesn't existclear()- Remove all files from the storage directorygetLocalUri(filename)- Get the full path for a filenamesaveFile(filePath, data, options?)- Save data to a filereadFile(filePath, options?)- Read a file as ArrayBuffermoveFile(sourceUri, targetUri)- Move a file into managed storage without buffering (enablessaveFileFromUri)deleteFile(filePath)- Delete a filefileExists(filePath)- Check if a file existsmakeDir(path)- Create a directoryrmDir(path)- Remove a directory
Implement the AttachmentTransportAdapter interface from @powersync/common:
upload(attachment)- Transfer the local file to remote storagedownload(attachment)- Transfer the remote file intoattachment.localUridelete(attachment)- Delete the file from remote storage
| Adapter | Library | Supported Versions |
|---|---|---|
ExpoFileSystemStorageAdapter |
expo-file-system |
>=19.0.0 (Expo 54+) |
ReactNativeFileSystemStorageAdapter |
@dr.pogodin/react-native-fs |
^2.25.0 |
Streaming transports are created via localStorage.createTransportAdapter(...). The Expo streaming transport additionally requires expo-file-system >=56 (Expo SDK 56+).
Apache-2.0