Skip to content

feat(PowerSync): add attachments support - #1616

Open
Chriztiaan wants to merge 18 commits into
TanStack:mainfrom
powersync-ja:feat/powersync-attachments
Open

feat(PowerSync): add attachments support#1616
Chriztiaan wants to merge 18 commits into
TanStack:mainfrom
powersync-ja:feat/powersync-attachments

Conversation

@Chriztiaan

@Chriztiaan Chriztiaan commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

🎯 Changes

Derived from Steven's efforts in powersync-ja/powersync-js#983, and addresses #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 TanStackDBAttachmentQueue that 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).

✅ Checklist

  • I have tested this code locally with pnpm test.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.

Summary by CodeRabbit

  • New Features
    • Added attachment support for PowerSync DB collections through a TanStack DB-backed attachment queue.
    • Supports atomic save and delete operations, custom attachment IDs, and updates to related records.
    • Improved synchronization and rollback for failed attachment operations.
  • Documentation
    • Added setup and usage guidance for managing attachments and displaying local attachment URLs.
  • Tests
    • Added coverage for attachment lifecycle, cleanup, custom IDs, and transaction rollback.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a TanStack DB-backed attachment queue with public exports, setup documentation, and tests. Attachments can be saved and deleted inside collection transactions, with examples for watching, syncing, linked-row updates, and cached URIs.

Changes

TanStackDB attachment queue

Layer / File(s) Summary
API surface and package wiring
packages/powersync-db-collection/src/attachments.ts, packages/powersync-db-collection/src/index.ts, packages/powersync-db-collection/package.json, .changeset/curly-planets-lead.md
Adds attachment queue types and implementation, exports them from the package entrypoint, updates PowerSync package versions, and records a minor release.
Queue setup and attachment watching
docs/collections/powersync-collection.md, packages/powersync-db-collection/tests/attachments.test.ts
Documents attachment collection setup, queue construction, sync lifecycle usage, and watchAttachments, with test setup for attachment state tracking.
Atomic save flow
packages/powersync-db-collection/src/attachments.ts, docs/collections/powersync-collection.md, packages/powersync-db-collection/tests/attachments.test.ts
Implements local persistence, queued uploads, transactional update hooks, rollback cleanup, synchronization, and caller-supplied IDs.
Atomic delete flow
packages/powersync-db-collection/src/attachments.ts, docs/collections/powersync-collection.md, packages/powersync-db-collection/tests/attachments.test.ts
Implements queued deletion and transactional linked-row updates, with tests for successful deletion, unknown IDs, and rollback behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TanStackDBAttachmentQueue
  participant attachmentsCollection
  participant updateHook
  TanStackDBAttachmentQueue->>attachmentsCollection: mutate attachment record
  TanStackDBAttachmentQueue->>updateHook: mutate linked collection row
  TanStackDBAttachmentQueue-->>TanStackDBAttachmentQueue: commit or rollback transaction
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding attachments support to PowerSync.
Description check ✅ Passed The description explains the problem and solution and includes completed testing and release-impact checklist items with a changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Chriztiaan
Chriztiaan marked this pull request as ready for review June 25, 2026 07:45

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/curly-planets-lead.md:
- Around line 1-3: The changeset for `@tanstack/powersync-db-collection` is
labeled patch, but this release introduces a new public export and raises the
`@powersync/common` peer minimum, so update the changeset in curly-planets-lead.md
to at least minor (or the appropriate breaking level per your policy). Keep the
package name the same and change only the release type so the generated release
notes reflect the expanded surface area and peer dependency bump.

In `@docs/collections/powersync-collection.md`:
- Around line 1195-1207: The watcher example in the referenced attachment-ID
flow is fire-and-forgetting onUpdate, which can let overlapping updates resolve
out of order. Update the example around livePhotoIds.stateWhenReady,
livePhotoIds.subscribeChanges, and the onUpdate callback to serialize each async
invocation by awaiting the previous one (for example through a chained promise
or equivalent queue) so the initial state and later change notifications are
processed in order. Also mirror the same awaited pattern in the test helper that
currently reproduces this race.

In `@packages/powersync-db-collection/src/attachments.ts`:
- Around line 79-80: The `addAttachment` flow in `Attachments` writes the blob
with `localStorage.saveFile()` before the transaction, but failures in `insert`,
`updateHook`, or `commit()` leave the file orphaned on disk. Update the
attachment write path to track the just-saved local URI and delete it on every
exception path, including transaction rollback and any thrown hook/commit error,
while keeping successful saves intact. Also add a regression test around
`updateHook` that throws to verify the DB changes roll back and the local file
is cleaned up.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f1f73be-698d-4312-886c-aeb36c8b4f13

📥 Commits

Reviewing files that changed from the base of the PR and between 45617c4 and cd8b191.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • .changeset/curly-planets-lead.md
  • docs/collections/powersync-collection.md
  • packages/powersync-db-collection/package.json
  • packages/powersync-db-collection/src/attachments.ts
  • packages/powersync-db-collection/src/index.ts
  • packages/powersync-db-collection/tests/attachments.test.ts

Comment thread .changeset/curly-planets-lead.md
Comment thread docs/collections/powersync-collection.md
Comment thread packages/powersync-db-collection/src/attachments.ts
@Chriztiaan

Copy link
Copy Markdown
Contributor Author

@samwillis - could you review this one please?

@tannerlinsley

Copy link
Copy Markdown
Member

I ran the exact package suite locally and all 92 tests pass, but I still need a few correctness fixes before merging this:

  • saveFile() runs before the collection transaction, so a failed insert, hook, or commit leaves an orphaned local file. Please clean it up on failure and add a regression test.
  • The docs show async updateHook callbacks, but the hook is called inside synchronous transaction.mutate() and its promise is ignored. Any mutation after an await escapes the transaction. The contract and examples need to be explicitly synchronous, or the implementation needs an actually awaited atomic design.
  • watchAttachments calls need to be serialized so an older async update can't win after a newer one.
  • This adds public API and raises the PowerSync peer floor from 1.41 to 1.57, so the changeset should be minor, not patch.

The earlier review threads are marked resolved, but the current diff still has these behaviors.

@Chriztiaan

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback!

  • saveFile() runs before the collection transaction, so a failed insert, hook, or commit leaves an orphaned local file. Please clean it up on failure and add a regression test.
    Done.
  • The docs show async updateHook callbacks, but the hook is called inside synchronous transaction.mutate() and its promise is ignored. Any mutation after an await escapes the transaction. The contract and examples need to be explicitly synchronous, or the implementation needs an actually awaited atomic design.
    Fixed outdated docs, this is no longer async.
  • watchAttachments calls need to be serialized so an older async update can't win after a newer one.
    I don't believe this comment is correct/needed. They should already be serialized (at least by our SDK).
  • This adds public API and raises the PowerSync peer floor from 1.41 to 1.57, so the changeset should be minor, not patch.
    Done.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/powersync-db-collection/src/attachments.ts (2)

36-41: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Document (and consider enforcing) that updateHook must be synchronous.

Transaction.mutate's callback wrapping only covers the synchronous portion of the callback: registerTransaction/unregisterTransaction wrap callback() directly, with unregisterTransaction in a finally that runs as soon as callback() returns. An async updateHook that does work after an await runs outside the transaction context, so its mutations would not be committed atomically with the attachment write — contradicting the doc's claim that "any mutations made to other collections are committed atomically with it." This is the exact concern flagged in the PR review (updateHook callbacks must be synchronous or implement an awaited atomic design) and is still not reflected in the docs or type signature.

Update the doc to state the requirement explicitly, and apply the same clarification to DeleteOptions.updateHook.

📝 Proposed doc fix
   /**
-   * Called within the same TanStackDB transaction as the attachment write,
-   * so any mutations made to other collections are committed atomically with it.
+   * Called synchronously within the same TanStackDB transaction as the attachment write,
+   * so any mutations made to other collections are committed atomically with it.
+   * An async callback escapes the transaction boundary: mutations made after an
+   * `await` inside this hook are not part of this transaction.
    */
   updateHook?: (attachment: AttachmentQueueRow) => void
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/powersync-db-collection/src/attachments.ts` around lines 36 - 41,
Update the updateHook documentation in the attachment write options to
explicitly require a synchronous callback and warn that asynchronous work after
an await is outside the transaction. Apply the same clarification to
DeleteOptions.updateHook, while preserving the existing callback type and
atomicity description.

73-96: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Guard against reusing an existing attachment id before writing the file.

The docstring for id (Line 31) already documents this: reusing an existing id overwrites that attachment's local file, the insert is then rejected, and cleanup deletes the overwritten file — leaving the pre-existing record without a file. save() writes the file at Line 84 before any check against this.collection, so this data-loss path is real, not just theoretical, and is easy to prevent.

Reject the id up front instead of documenting the footgun.

🛡️ Proposed fix
     const resolvedId = id ?? (await this.generateAttachmentId())
+    if (id !== undefined && this.collection.get(resolvedId)) {
+      throw new Error(`Attachment with id ${resolvedId} already exists`)
+    }
     const filename = `${resolvedId}.${fileExtension}`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/powersync-db-collection/src/attachments.ts` around lines 73 - 96,
Update AttachmentQueue.save to validate an explicitly provided id against
this.collection before calling localStorage.saveFile. Reject existing attachment
ids with an error and preserve normal id generation and saving for new
attachments.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/powersync-db-collection/src/attachments.ts`:
- Around line 36-41: Update the updateHook documentation in the attachment write
options to explicitly require a synchronous callback and warn that asynchronous
work after an await is outside the transaction. Apply the same clarification to
DeleteOptions.updateHook, while preserving the existing callback type and
atomicity description.
- Around line 73-96: Update AttachmentQueue.save to validate an explicitly
provided id against this.collection before calling localStorage.saveFile. Reject
existing attachment ids with an error and preserve normal id generation and
saving for new attachments.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 47ee24a9-dbe5-456e-9288-09f20abaf25b

📥 Commits

Reviewing files that changed from the base of the PR and between 2250d35 and 2718e1b.

📒 Files selected for processing (4)
  • .changeset/curly-planets-lead.md
  • docs/collections/powersync-collection.md
  • packages/powersync-db-collection/src/attachments.ts
  • packages/powersync-db-collection/tests/attachments.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • .changeset/curly-planets-lead.md
  • docs/collections/powersync-collection.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/powersync-db-collection/src/attachments.ts`:
- Around line 34-43: Update the SaveOptions and DeleteOptions updateHook types
to reject Promise or thenable return values, and add runtime checks around both
save and delete hook invocation to detect a returned thenable and fail before it
can escape the transaction. Add a regression test covering an async updateHook
and verify that mutations after await are not committed outside the attachment
transaction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1c16d74c-cec4-4138-94d6-b0f7dde63c58

📥 Commits

Reviewing files that changed from the base of the PR and between 2718e1b and 5d3b500.

📒 Files selected for processing (2)
  • packages/powersync-db-collection/src/attachments.ts
  • packages/powersync-db-collection/tests/attachments.test.ts

Comment thread packages/powersync-db-collection/src/attachments.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants