feat(PowerSync): add attachments support#9
Conversation
stevensJourney
left a comment
There was a problem hiding this comment.
Overall I'm happy with the approach here. Left a few comments, mostly nits.
| } from '@powersync/common' | ||
| import type { Collection } from '@tanstack/db' | ||
|
|
||
| export type AttachmentQueueRow = (typeof _tmpSchema)['types']['attachments'] |
There was a problem hiding this comment.
Nit: Can't we use AttachmentRecord here?
There was a problem hiding this comment.
Ah I see now!
I think the best fix would be to export some record level type from the common SDK. After checking, the type we have here isn't actually very useful (does not contain the actual columns)
The general type extraction utilities fail for AttachmentTable since this the columns are only defined inside the constructor's super call.
In the common SDK, something like this could work
const ATTACHMENT_TABLE_COLUMNS = {
filename: column.text,
local_uri: column.text,
timestamp: column.integer,
size: column.integer,
media_type: column.text,
state: column.integer, // Corresponds to AttachmentState
has_synced: column.integer,
meta_data: column.text
};
/**
* AttachmentTable defines the schema for the attachment queue table.
*
* @alpha
*/
export class AttachmentTable extends Table<typeof ATTACHMENT_TABLE_COLUMNS> {
constructor(options?: AttachmentTableOptions) {
super(ATTACHMENT_TABLE_COLUMNS, {
...options,
viewName: options?.viewName ?? ATTACHMENT_TABLE,
localOnly: true,
insertOnly: false
});
}
}
export type AttachmentTableRecord = RowType<AttachmentTable>;| export interface DeleteFileTanStackOptions { | ||
| id: string | ||
| /** * | ||
| * Note that this is called inside a synchronous TanStackDB transaction, |
There was a problem hiding this comment.
If we call this in a synchronous transaction invocation, how can this method reliably be async?
| `saveFileTanStack` writes the file, inserts the attachment record into your collection, and runs your `updateHook` mutations in the same transaction. Use the hook to insert or update the row that references the new attachment, so both land together or not at all. | ||
|
|
||
| ```ts | ||
| await attachmentQueue.saveFileTanStack({ |
There was a problem hiding this comment.
It's a nit, and I don't have the strongest opinion on this, but the saveFileTanStack is a bit of a mouthful. Maybe save (just to distinguish it from the base method)?

Derived from Steven's efforts in powersync-ja/powersync-js#983, and addresses TanStack#1563.
Problem
PowerSync ships an attachment helper for syncing files (photos, documents) between local and remote storage. It's separate from regular synced tables: a local-only attachments table tracks each file's lifecycle (QUEUED_UPLOAD, SYNCED, QUEUED_DELETE), and an AttachmentQueue drives uploads/downloads in the background.
TanStackDB, on the other hand, gives you an optimistic, reactive, joinable view over synced data. For users who want to use the attachment helper alongside the PowerSync+TanstackDB integration there are blockers. Saving a file (in the local-only attachments table) and associating it with a record (e.g. setting user.photo_id) are two independent writes which could make data races and fatal errors a problem for data consistency.
The original POC (powersync-js#983) proved this integration was viable. This PR productionises a a subset of it as reusable functionality.
Solution
A
TanStackDBAttachmentQueuethat extends the SDK's AttachmentQueue (for saving and deleting a file) and backs it with a TanStack DB collection.The package owns the collection-backed saveFile/delete implementation and leaves the wiring to the application (covered in documentation).
Future Work
After this has been released, we can merge the changes made to the PowerSync JS TanstackDB demo.
AI Disclosure
I used Claude Opus to help investigate, implement, and verify this work.