Skip to content

feat(PowerSync): add attachments support - #1616

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

feat(PowerSync): add attachments support#1616
Chriztiaan wants to merge 13 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, including a TanStackDB-backed attachment queue with atomic save/delete flows.
    • Supports optional per-operation update hooks to apply related data changes within the same transaction.
    • Re-exported the new attachment queue from the package entrypoint.
  • Documentation
    • Added an “Attachments” section with setup, saving, displaying, and removing guidance.
  • Tests
    • Added a dedicated test suite covering upload/delete lifecycle behavior and hook atomicity.
  • Bug Fixes
    • Improved reliability of keeping local files, queued attachment states, and related records in sync.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bb746259-b2d9-4df9-94d5-7bfb2e205237

📥 Commits

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

📒 Files selected for processing (1)
  • packages/powersync-db-collection/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/powersync-db-collection/package.json

📝 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 covering watching, syncing, detaching references, and reading 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 patch release.
Queue setup and attachment watching
docs/collections/powersync-collection.md, packages/powersync-db-collection/tests/attachments.test.ts
Documents TanStack DB collection setup and watchAttachments, with shared test setup for attachment state tracking.
Atomic save flow and attachment readback
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, sync transitions, caller-supplied IDs, and live-query access to cached attachment URIs.
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 foreign-key clearing, with tests for synchronization cleanup and unknown IDs.

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

Suggested reviewers: kyleamathews, samwillis

Sequence Diagram(s)

sequenceDiagram
  participant TanStackDBAttachmentQueue
  participant withAttachmentContext
  participant PowerSyncTransactor
  participant attachmentsCollection
  participant updateHook
  TanStackDBAttachmentQueue->>withAttachmentContext: delete()
  withAttachmentContext->>PowerSyncTransactor: open non-auto-committing transaction
  PowerSyncTransactor->>attachmentsCollection: load attachment by id
  PowerSyncTransactor->>attachmentsCollection: set QUEUED_DELETE and reset has_synced
  TanStackDBAttachmentQueue->>updateHook: clear linked foreign key
  PowerSyncTransactor-->>TanStackDBAttachmentQueue: commit
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 is concise and accurately summarizes the main change: adding attachments support for PowerSync/TanStack DB.
Description check ✅ Passed The description covers changes, motivation, checklist, and release impact, matching the required template well enough.
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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
packages/powersync-db-collection/package.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


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.

@socket-security

socket-security Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​powersync/​node@​0.19.27810010098100
Addednpm/​@​powersync/​common@​1.57.0981008098100

View full report

@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 on lines +1 to +3
---
'@tanstack/powersync-db-collection': patch
---

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

patch is too small for this release.

This PR adds a new public export and raises the package's @powersync/common peer minimum from ^1.41.0 to ^1.57.0. Shipping that as a patch hides a new surface area and can break consumers that have not upgraded their PowerSync peer yet. Please bump this changeset to at least minor (or whatever level your peer-dependency policy treats as breaking).

🤖 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 @.changeset/curly-planets-lead.md around lines 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.

Comment on lines +1195 to +1207
// 1. Report the initial set of referenced attachment IDs.
const initialState = await livePhotoIds.stateWhenReady()
onUpdate(Array.from(initialState.values()).map(mapper))

// 2. Re-emit the whole set on every change (the queue expects the holistic state).
livePhotoIds.subscribeChanges(() => {
onUpdate(livePhotoIds.map(mapper))
})

// 3. Clean up when sync stops.
abortSignal.addEventListener("abort", () => livePhotoIds.cleanup(), {
once: true,
})

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Serialize the async onUpdate calls in this example.

The watcher example fire-and-forgets onUpdate for both the initial state and subsequent changes. If two updates arrive close together, those promises can resolve out of order and stale attachment sets can win. Please show an awaited/serialized pattern here; the test helper currently mirrors the same race.

🤖 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 `@docs/collections/powersync-collection.md` around lines 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.

Comment on lines +79 to +80
const localUri = this.localStorage.getLocalUri(filename)
const size = await this.localStorage.saveFile(localUri, data)

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Roll back the saved file when the transaction fails.

saveFile() runs before the collection transaction starts. If insert, updateHook, or commit() throws, the DB work is rolled back but the blob stays on disk, so this API is no longer atomic and leaks orphaned local files. Please clean up the just-written file on every failure path and add a regression test that throws from updateHook.

Also applies to: 99-116

🤖 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 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.

@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.

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