Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
01070e1
initial implementation
joshua-journey-apps Oct 6, 2025
23dc8e4
Add make, remove and get uri endpoints to local storage
joshua-journey-apps Oct 7, 2025
ecb71cb
Fix exporting node and index db storage adapters
joshua-journey-apps Oct 7, 2025
029df9d
Wip add sync throttle & cache limiting
joshua-journey-apps Oct 7, 2025
958675a
Add downloading attachment test
joshua-journey-apps Oct 7, 2025
45773aa
Add user defined storage adapter path
joshuabrink Oct 27, 2025
533dfab
Refactor watch active observer into dedicated service
joshuabrink Oct 27, 2025
643289d
Add temporal units to variable name
joshuabrink Oct 28, 2025
6840fb0
Rename storage -> syncing service
joshuabrink Oct 28, 2025
d8d4ad9
Add updateHook to save file
joshuabrink Oct 28, 2025
9509aeb
Use async onUpdate callback
joshuabrink Oct 28, 2025
9ac685c
Improve comments
joshuabrink Oct 28, 2025
ce33bab
Fix closing the watch active attachments listener
joshuabrink Oct 28, 2025
704c2a8
tests(WIP:) initial node setup and few bug fixes
khawarizmus Oct 31, 2025
ff25b2b
tests: fixed SyncService to rely on LocalStorage and be agnostic to f…
khawarizmus Oct 31, 2025
fae7f27
test: workflow tests working and passing
khawarizmus Nov 5, 2025
eedd224
feat: introduce AttachmentErrorHandler for custom error handling in a…
khawarizmus Nov 5, 2025
ec1a733
feat: added archival management to AttachmentQueue via AttachmentCon…
khawarizmus Nov 6, 2025
f6b2343
docs: update README.md to document usage
khawarizmus Nov 6, 2025
4cb5753
feat: added ExpoFileSystemAdapter to the attachments package
khawarizmus Nov 10, 2025
7e40d26
refactor: refactored react native demo to use new attachments API
khawarizmus Nov 10, 2025
206bd5f
fix: expo blob compatability and attachment row mapping
joshuabrink Nov 12, 2025
e42ae45
fix: node exclusive exports
joshuabrink Nov 12, 2025
4d5a1bc
fix: linting, dead code and move active watch into start sync
joshuabrink Nov 12, 2025
8db8f2a
refactor: move attachments to common package
joshuabrink Nov 20, 2025
be856e9
refactor: moved readme to attachments folder
khawarizmus Nov 27, 2025
c3fd550
refactor: add ExpoFileSystemAdapter for local storage management in R…
khawarizmus Nov 27, 2025
26722fb
fix: export IndexDBFileSystemAdapter from web package
khawarizmus Nov 28, 2025
3eba6f0
chore: add helper script to clean up node_modules directories in the …
khawarizmus Nov 28, 2025
de9130e
docs: fixed README to properly point to React Native web demo
khawarizmus Nov 28, 2025
20bb3a3
refactor: demos now use attachments functionality from the core libra…
khawarizmus Nov 28, 2025
b8dea39
refactor: migrate to using @powersync/common for shared functionality…
khawarizmus Nov 28, 2025
62b0bdc
fix: exclude expo file system from bundle
khawarizmus Nov 28, 2025
d514ef8
Merge branch 'main' into attachment-package-refactor
khawarizmus Nov 28, 2025
649290c
Merge branch 'main' into attachment-package-refactor
khawarizmus Nov 28, 2025
eb0a93a
fix: add missing comma in package.json dependencies
khawarizmus Nov 28, 2025
d179b9c
chore: update dependencies in pnpm-lock.yaml to latest versions
khawarizmus Nov 28, 2025
f401bc0
fix: update import paths in attachment-related files to use local mod…
khawarizmus Nov 28, 2025
d86799a
chore: add changeset
khawarizmus Nov 28, 2025
08e062f
chore: update deprecation message
joshuabrink Dec 9, 2025
0886ae0
fix: should retry handling error as default
joshuabrink Dec 9, 2025
871d3dd
chore: move attachment tests to node package
joshuabrink Dec 9, 2025
3b6a8fe
chore: fix comment
joshuabrink Dec 9, 2025
e9ac51a
chore: conform verifyAttachments to normal function
joshuabrink Dec 9, 2025
ecd22e5
feat: add abortable attachment watchers
joshuabrink Dec 12, 2025
8cfc680
feat: add vanilla react native storage adapter
joshuabrink Dec 12, 2025
aec02b1
fix: web attachment display using blob url
joshuabrink Dec 12, 2025
9812964
fix: export expo fs adapter
joshuabrink Dec 12, 2025
4946f37
refactor: implement watchAttachments as a class property
joshuabrink Dec 22, 2025
46fac0f
refactor: AttachmentQueue options as immutable config and improve log…
joshuabrink Dec 22, 2025
65c6384
fix: use private property for timer assignment
joshuabrink Dec 22, 2025
5ef0257
chore: add adapter deps to rollup external and dev dependencies
joshuabrink Dec 22, 2025
12f533f
fix: add lazy import to constructor and improve error handling
joshuabrink Dec 22, 2025
03996bb
chore: update expo file system depedency
joshuabrink Dec 29, 2025
73a3c06
refactor: enforce exclusive database access via AttachmentService
joshuabrink Dec 29, 2025
bd6a5b9
refactor: improve separation of third party file system dependencies
joshuabrink Dec 29, 2025
875ea0e
chore: use changeset file for CI generated CHANGELOG
joshuabrink Jan 6, 2026
d5877c6
fix: ArrayBuffer saveFile return type
joshuabrink Jan 6, 2026
c1bef91
clean: remove unused QUEUED_SYNCED enum
joshuabrink Jan 6, 2026
e3aa49a
improve: property and parameter data types
joshuabrink Jan 6, 2026
41ea536
fix: property access visibility
joshuabrink Jan 6, 2026
07a5e03
fix: retry variable type, from promise to boolean
joshuabrink Jan 6, 2026
f5b62ce
improve: reprocess attachments as soon as device is online
joshuabrink Jan 6, 2026
c6a1b76
fix: handle custom filename usage
joshuabrink Jan 6, 2026
ff10f82
fix: include timestamp for newly created attachments updates
joshuabrink Jan 6, 2026
7f8323b
fix: await usage and potential inifinte loop
joshuabrink Jan 6, 2026
109621c
feat: add multi-version expo file system support
joshuabrink Jan 6, 2026
744b36f
fix: type imports
joshuabrink Jan 6, 2026
d68d285
improve: error messages + encoding parameter + unnecessary try catch …
joshuabrink Jan 6, 2026
ed97c67
fix: potential reinsertion of deleted attachment
joshuabrink Jan 6, 2026
8e733c9
fix: set historical has synced on downloaded attachments
joshuabrink Jan 6, 2026
f19a9f5
refactor: demos to use new react native attachments package
joshuabrink Jan 6, 2026
5ebceeb
refactor: only support expo 54+ file system
joshuabrink Jan 8, 2026
bdf8cd2
improve: base64 usage
joshuabrink Jan 8, 2026
8219606
improve: types and error handling
joshuabrink Jan 8, 2026
0b7531d
improve: package naming and author field
joshuabrink Jan 9, 2026
a9ebf9a
Merge branch 'main' into attachment-package-refactor
joshuabrink Jan 9, 2026
4dbe852
fix: duplicate lock file deps from merge
joshuabrink Jan 9, 2026
e4f29ed
fix: file system adapter incompatability for Expo 52
joshuabrink Jan 9, 2026
b3aa3b8
chore: add experimental tags
joshuabrink Jan 12, 2026
ce443f4
chore: add internal and experimental annotations
joshuabrink Jan 13, 2026
586231d
chore: update readme docs with alpha notice and include library in root
joshuabrink Jan 13, 2026
9f722c0
Merge branch 'main' into attachment-package-refactor
joshuabrink Jan 15, 2026
81d2448
fix: attachments import path
joshuabrink Jan 15, 2026
3553042
chore: include the Expo 54 requirement for the adapter
joshuabrink Jan 19, 2026
c821850
fix: archived attachments delete query
joshuabrink Jan 20, 2026
7248743
fix: broken tests and refactor recovery check
joshuabrink Jan 20, 2026
bdc9750
fix: format issues
joshuabrink Jan 20, 2026
cefd391
fix: remove directory for indexdb adapter
joshuabrink Jan 20, 2026
a432bac
chore: surface attachments readme
joshuabrink Jan 21, 2026
f0bc5c0
chore: set the base version to 0.0.1
joshuabrink Jan 27, 2026
3011045
fix: corresponding enum in readme
joshuabrink Jan 27, 2026
e55c74e
fix: exit watch attachments if signal is aborted
joshuabrink Jan 27, 2026
384379e
chore: remove unused functions and improve clear promise usage
joshuabrink Jan 27, 2026
24a7114
refactor: demos to use new attachment helper library
joshuabrink Jan 27, 2026
43a00d6
fix: classes included in changeset message
joshuabrink Jan 27, 2026
93fe737
Merge branch 'main' into attachment-package-refactor
joshuabrink Jan 27, 2026
6212b33
fix: broken lock file
joshuabrink Jan 27, 2026
7adf99d
fix: broken node attachment tests
joshuabrink Jan 27, 2026
69dd241
fix: broken tests
joshuabrink Jan 27, 2026
5c0d24b
fix: better-sqlite3 binding issues for node tests
joshuabrink Jan 28, 2026
ed32db1
fix: remove pnpm dependency build filter
joshuabrink Jan 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/angry-planes-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@powersync/react-native': minor
'@powersync/common': minor
'@powersync/node': minor
'@powersync/web': minor
'@powersync/attachments': patch
---

Deprecated @powersync/attachments in favor of enhanced and consistent attachment functionality built into @powersync/common and platform-specific SDKs
5 changes: 5 additions & 0 deletions .changeset/new-attachments-react-native.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/attachments-storage-react-native': minor
---

Added new @powersync/attachments-storage-react-native package providing LocalStorageAdapter implementations for React Native environments. Includes ExpoFileSystemStorageAdapter and ReactNativeFileSystemStorageAdapter for device-based attachment file storage.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ _[PowerSync](https://www.powersync.com) is a sync engine for building local-firs

- [packages/attachments](./packages/attachments/README.md)

- Attachments helper package for React Native and JavaScript/TypeScript projects.
- Attachments helper package for React Native and JavaScript/TypeScript projects (deprecated).

- [packages/attachments-storage-react-native](./packages/attachments-storage-react-native/README.md)

- React Native file system storage adapters for PowerSync attachments (alpha).

- [packages/kysely-driver](./packages/kysely-driver/README.md)

Expand All @@ -51,7 +55,7 @@ _[PowerSync](https://www.powersync.com) is a sync engine for building local-firs
- [OP-SQLite](https://github.com/OP-Engineering/op-sqlite) integration for React Native projects. Alternative to the default usage of [react-native-quick-sqlite](https://github.com/powersync-ja/react-native-quick-sqlite).

- [packages/common](./packages/common/README.md)
- Shared package: TypeScript implementation of a PowerSync database connector and streaming sync bucket implementation.
- Shared package: TypeScript implementation of a PowerSync database connector, streaming sync bucket implementation and attachment utilities.

## Demo Apps / Example Projects

Expand Down
51 changes: 22 additions & 29 deletions demos/react-native-supabase-todolist/app/views/todos/edit/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ATTACHMENT_TABLE, AttachmentRecord } from '@powersync/attachments';
import { usePowerSync, useQuery } from '@powersync/react-native';
import { usePowerSync, useQuery, ATTACHMENT_TABLE, attachmentFromSql, AttachmentRecord } from '@powersync/react-native';
import { CameraCapturedPicture } from 'expo-camera';
import _ from 'lodash';
import * as React from 'react';
Expand All @@ -12,21 +11,7 @@ import { TODO_TABLE, TodoRecord, LIST_TABLE } from '../../../../library/powersyn
import { useSystem } from '../../../../library/powersync/system';
import { TodoItemWidget } from '../../../../library/widgets/TodoItemWidget';

type TodoEntry = TodoRecord & Partial<Omit<AttachmentRecord, 'id'>> & { todo_id: string; attachment_id: string | null };

const toAttachmentRecord = _.memoize((entry: TodoEntry): AttachmentRecord | null => {
return entry.attachment_id == null
? null
: {
id: entry.attachment_id,
filename: entry.filename!,
state: entry.state!,
timestamp: entry.timestamp,
local_uri: entry.local_uri,
media_type: entry.media_type,
size: entry.size
};
});
type TodoEntry = TodoRecord & { todo_id: string; attachment_id: string | null };

const TodoView: React.FC = () => {
const system = useSystem();
Expand Down Expand Up @@ -61,10 +46,10 @@ const TodoView: React.FC = () => {
if (completed) {
const userID = await system.supabaseConnector.userId();
updatedRecord.completed_at = new Date().toISOString();
updatedRecord.completed_by = userID;
updatedRecord.completed_by = userID!;
} else {
updatedRecord.completed_at = undefined;
updatedRecord.completed_by = undefined;
updatedRecord.completed_at = null;
updatedRecord.completed_by = null;
}
await system.powersync.execute(
`UPDATE ${TODO_TABLE}
Expand All @@ -77,9 +62,13 @@ const TodoView: React.FC = () => {
};

const savePhoto = async (id: string, data: CameraCapturedPicture) => {
if (system.attachmentQueue) {
if (system.photoAttachmentQueue) {
// We are sure the base64 is not null, as we are using the base64 option in the CameraWidget
const { id: photoId } = await system.attachmentQueue.savePhoto(data.base64!);
const { id: photoId } = await system.photoAttachmentQueue.saveFile({
data: data.base64!,
fileExtension: 'jpg',
mediaType: 'image/jpeg'
});

await system.powersync.execute(`UPDATE ${TODO_TABLE} SET photo_id = ? WHERE id = ?`, [photoId, id]);
}
Expand All @@ -99,12 +88,16 @@ const TodoView: React.FC = () => {
};

const deleteTodo = async (id: string, photoRecord?: AttachmentRecord) => {
await system.powersync.writeTransaction(async (tx) => {
if (system.attachmentQueue && photoRecord != null) {
await system.attachmentQueue.delete(photoRecord, tx);
}
await tx.execute(`DELETE FROM ${TODO_TABLE} WHERE id = ?`, [id]);
});
if (system.photoAttachmentQueue && photoRecord != null) {
await system.photoAttachmentQueue.deleteFile({
id: photoRecord.id,
updateHook: async (tx) => {
await tx.execute(`DELETE FROM ${TODO_TABLE} WHERE id = ?`, [id]);
}
});
} else {
await system.powersync.execute(`DELETE FROM ${TODO_TABLE} WHERE id = ?`, [id]);
}
};

if (isLoading) {
Expand Down Expand Up @@ -157,7 +150,7 @@ const TodoView: React.FC = () => {
<ScrollView style={{ maxHeight: '90%' }}>
{todos.map((r) => {
const record = { ...r, id: r.todo_id };
const photoRecord = toAttachmentRecord(r);
const photoRecord = attachmentFromSql(r);
return (
<TodoItemWidget
key={r.todo_id}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AttachmentTable } from '@powersync/attachments';
import { column, Schema, Table } from '@powersync/react-native';
import { AttachmentTable } from '@powersync/common';

export const LIST_TABLE = 'lists';
export const TODO_TABLE = 'todos';
Expand Down Expand Up @@ -27,9 +27,7 @@ const lists = new Table({
export const AppSchema = new Schema({
todos,
lists,
attachments: new AttachmentTable({
name: 'attachments',
}),
attachments: new AttachmentTable(),
});

export type Database = (typeof AppSchema)['types'];
Expand Down

This file was deleted.

85 changes: 62 additions & 23 deletions demos/react-native-supabase-todolist/library/powersync/system.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
import '@azure/core-asynciterator-polyfill';

import { createBaseLogger, LogLevel, PowerSyncDatabase, SyncClientImplementation } from '@powersync/react-native';
import {
createBaseLogger,
LogLevel,
PowerSyncDatabase,
SyncClientImplementation,
AttachmentQueue,
type AttachmentRecord,
type WatchedAttachmentItem,
} from '@powersync/react-native';
import { ReactNativeFileSystemStorageAdapter } from '@powersync/attachments-storage-react-native';
import React from 'react';
import { SupabaseStorageAdapter } from '../storage/SupabaseStorageAdapter';

import { type AttachmentRecord } from '@powersync/attachments';
import { configureFts } from '../fts/fts_setup';
import { KVStorage } from '../storage/KVStorage';
import { SupabaseRemoteStorageAdapter } from '../storage/SupabaseRemoteStorageAdapter';
import { AppConfig } from '../supabase/AppConfig';
import { SupabaseConnector } from '../supabase/SupabaseConnector';
import { AppSchema } from './AppSchema';
import { PhotoAttachmentQueue } from './PhotoAttachmentQueue';
import { AppSchema, TODO_TABLE } from './AppSchema';

const logger = createBaseLogger();
logger.useDefaults();
logger.setLevel(LogLevel.DEBUG);

export class System {
kvStorage: KVStorage;
storage: SupabaseStorageAdapter;
supabaseConnector: SupabaseConnector;
powersync: PowerSyncDatabase;
attachmentQueue: PhotoAttachmentQueue | undefined = undefined;
photoAttachmentQueue: AttachmentQueue | undefined = undefined;

constructor() {
this.kvStorage = new KVStorage();
this.supabaseConnector = new SupabaseConnector(this);
this.storage = this.supabaseConnector.storage;
this.supabaseConnector = new SupabaseConnector({
kvStorage: this.kvStorage,
supabaseUrl: AppConfig.supabaseUrl,
supabaseAnonKey: AppConfig.supabaseAnonKey
});

this.powersync = new PowerSyncDatabase({
schema: AppSchema,
database: {
Expand All @@ -50,18 +59,48 @@ export class System {
*/

if (AppConfig.supabaseBucket) {
this.attachmentQueue = new PhotoAttachmentQueue({
powersync: this.powersync,
storage: this.storage,
// Use this to handle download errors where you can use the attachment
// and/or the exception to decide if you want to retry the download
onDownloadError: async (attachment: AttachmentRecord, exception: any) => {
if (exception.toString() === 'StorageApiError: Object not found') {
return { retry: false };
}
const localStorage = new ReactNativeFileSystemStorageAdapter();
const remoteStorage = new SupabaseRemoteStorageAdapter({
client: this.supabaseConnector.client,
bucket: AppConfig.supabaseBucket
});

this.photoAttachmentQueue = new AttachmentQueue({
db: this.powersync,
localStorage,
remoteStorage,
watchAttachments: async (onUpdate, signal) => {
const watcher = this.powersync.watch(
`SELECT photo_id as id FROM ${TODO_TABLE} WHERE photo_id IS NOT NULL`,
[],
{
signal
}
);

return { retry: true };
}
for await (const result of watcher) {
const attachments: WatchedAttachmentItem[] = (result.rows?._array ?? []).map((row: any) => ({
id: row.id,
fileExtension: 'jpg'
}));
await onUpdate(attachments);
}
},
errorHandler: {
onDownloadError: async (attachment: AttachmentRecord, error: Error) => {
if (error.toString() === 'StorageApiError: Object not found') {
return false; // Don't retry
}
return true; // Retry
},
onUploadError: async (attachment: AttachmentRecord, error: Error) => {
return true; // Retry uploads by default
},
onDeleteError: async (attachment: AttachmentRecord, error: Error) => {
return true; // Retry deletes by default
}
},
logger
});
}
}
Expand All @@ -70,8 +109,8 @@ export class System {
await this.powersync.init();
await this.powersync.connect(this.supabaseConnector, { clientImplementation: SyncClientImplementation.RUST });

if (this.attachmentQueue) {
await this.attachmentQueue.init();
if (this.photoAttachmentQueue) {
await this.photoAttachmentQueue.startSync();
}

// Demo using SQLite Full-Text Search with PowerSync.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { SupabaseClient } from '@supabase/supabase-js';
import { AttachmentRecord, RemoteStorageAdapter } from '@powersync/react-native';

export interface SupabaseRemoteStorageAdapterOptions {
client: SupabaseClient;
bucket: string;
}

/**
* SupabaseRemoteStorageAdapter implements RemoteStorageAdapter for Supabase Storage.
* Handles upload, download, and deletion of files from Supabase Storage buckets.
*/
export class SupabaseRemoteStorageAdapter implements RemoteStorageAdapter {
constructor(private options: SupabaseRemoteStorageAdapterOptions) {}

async uploadFile(fileData: ArrayBuffer, attachment: AttachmentRecord): Promise<void> {
const mediaType = attachment.mediaType ?? 'application/octet-stream';

const { error } = await this.options.client.storage
.from(this.options.bucket)
.upload(attachment.filename, fileData, { contentType: mediaType });

if (error) {
throw error;
}
}

async downloadFile(attachment: AttachmentRecord): Promise<ArrayBuffer> {
const { data, error } = await this.options.client.storage.from(this.options.bucket).download(attachment.filename);

if (error) {
throw error;
}

return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
resolve(reader.result as ArrayBuffer);
};
reader.onerror = reject;
reader.readAsArrayBuffer(data);
});
}

async deleteFile(attachment: AttachmentRecord): Promise<void> {
const { error } = await this.options.client.storage.from(this.options.bucket).remove([attachment.filename]);

if (error) {
console.debug('Failed to delete file from Supabase Storage', error);
throw error;
}
}
}
Loading