Skip to content

fix(deploy): cache the PowerSync wasm and stop caching the SPA shell - #1205

Open
njbrake wants to merge 3 commits into
thunderbird:mainfrom
njbrake:pr6-static-asset-caching
Open

fix(deploy): cache the PowerSync wasm and stop caching the SPA shell#1205
njbrake wants to merge 3 commits into
thunderbird:mainfrom
njbrake:pr6-static-asset-caching

Conversation

@njbrake

@njbrake njbrake commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Note: this PR description was drafted by Claude via back-and-forth with @njbrake. The reasoning and decisions are his; the prose is Claude's.

Two nginx caching gaps that pull in opposite directions.

/@powersync/ was matched by no location block, so PowerSync's wa-sqlite assets fell through to location / with no caching directives at all. That is roughly 17MB of content-hashed files revalidated on every boot, when they are as immutable as anything under /assets/.

index.html had the opposite problem. It is the shell that names the hashed entry chunk, so a cached copy pins a client to an old build after a deploy. It now sends no-cache, which still permits a conditional request, so the common case is a 304 rather than a re-download.

Deep links pick this up too: the SPA fallback is a URI, so nginx does an internal redirect and re-runs location matching, and a request for /settings ends up in the location = /index.html block.

_Note: this PR description was drafted by Claude via back-and-forth with @njbrake. The reasoning and decisions are his; the prose is Claude's._

Two nginx caching gaps that pull in opposite directions.

**`/@powersync/` was matched by no location block**, so PowerSync's wa-sqlite assets fell through to `location /` with no caching directives at all. That is roughly 17MB of content-hashed files revalidated on every boot, when they are as immutable as anything under `/assets/`.

**`index.html` had the opposite problem.** It is the shell that names the hashed entry chunk, so a cached copy pins a client to an old build after a deploy. It now sends `no-cache`, which still permits a conditional request, so the common case is a 304 rather than a re-download.

Deep links pick this up too: the SPA fallback is a URI, so nginx does an internal redirect and re-runs location matching, and a request for `/settings` ends up in the `location = /index.html` block.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@njbrake
njbrake force-pushed the pr6-static-asset-caching branch from 6e4f0f8 to e92aa24 Compare August 13, 2026 15:57
@njbrake
njbrake deployed to fork-preview-approval August 13, 2026 15:58 — with GitHub Actions Active
@github-actions

Copy link
Copy Markdown

Preview environment deployed 🚀

Service URL
Marketing / blog / docs https://thunderbolt-pr-1205.preview.thunderbolt.io
App https://app-pr-1205.preview.thunderbolt.io
API https://api-pr-1205.preview.thunderbolt.io
Keycloak https://auth-pr-1205.preview.thunderbolt.io
PowerSync https://powersync-pr-1205.preview.thunderbolt.io

Stack: preview-pr-1205 · Commit: e92aa249a8c33c1df199606a04e0de5cce549ea5

Auto-destroys on PR close/merge. Login via the bundled Keycloak realm — demo@thunderbolt.io / demo by default.

@ital0 ital0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this one. The diagnosis is right, and the index.html part checks out: the 304 flow and the internal redirect for deep links both work as described.

My one concern is the /@powersync/ block. That folder isn't all content-hashed: the two workers (worker/WASQLiteDB.umd.js, worker/SharedSyncImplementation.umd.js) have stable names and are loaded by fixed path in src/db/powersync/database.ts:108,113, so a year of immutable can leave Safari web clients on an old worker after a @powersync/web upgrade. That's the same stale-build problem the index.html half of this PR fixes. More in the line comment.

Two smaller notes: the ~17MB figure includes .map files the map rule already 404s (what can be served is closer to 10MB), and oauth-callback.html plus the other public/ statics still have no cache policy. Both may well be intentional, just flagging.

# `3322bc84de986b63c2cd.wasm`, so they are as immutable as /assets/ -- and they
# total ~17MB, which is worth not revalidating on every boot. Without this they
# fall through to `location /`, which sets no caching directives at all.
location /@powersync/ {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One thing worries me here: not everything in this folder is content-hashed. copy-assets also emits index.umd.js and two workers (worker/WASQLiteDB.umd.js, worker/SharedSyncImplementation.umd.js) with stable names, and src/db/powersync/database.ts:108,113 loads those workers by fixed path (the config Safari uses on the web). With immutable for a year, a Safari client can stay on an old worker long after a @powersync/web upgrade, which is the same stale-build problem the index.html block below is fixing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in cbfad77. Split the block so ~ ^/@powersync/.*\.umd\.js$ serves the stable-named workers (and index.umd.js) no-cache, revalidating like index.html; only the content-hashed wasm keeps immutable. A @powersync/web upgrade can no longer strand a client on an old worker. Also gave oauth-callback.html its own no-cache rule; left the other public/ statics (favicons, capture-worklet.js) falling through, since they aren't content-hashed and the worklet loads by fixed path.

Note: this reply was drafted by Claude Opus 4.8 via back-and-forth with @njbrake. The reasoning and decisions are his; the prose is Claude's.

Comment thread deploy/config/nginx.conf.template Outdated
# PowerSync's wa-sqlite wasm/worker assets, copied into public/@powersync by
# the `copy-powersync-assets` Vite plugin. Content-hashed filenames like
# `3322bc84de986b63c2cd.wasm`, so they are as immutable as /assets/ -- and they
# total ~17MB, which is worth not revalidating on every boot. Without this they

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Small thing: the ~17MB counts the .map files, and the .map rule below already 404s those. What can actually be served is closer to 10MB.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in cbfad77. The comment now says ~10MB servable and notes the larger on-disk figure includes the .map files this rule 404s.

Note: this reply was drafted by Claude Opus 4.8 via back-and-forth with @njbrake. The reasoning and decisions are his; the prose is Claude's.

- The .umd.js workers have stable, fixed-path names, so `immutable`
  would pin clients to an old worker across a @powersync/web upgrade.
  Serve them no-cache (cheap 304) like index.html; keep immutable only
  for the content-hashed wasm.
- The oauth-callback.html entry document should reflect the latest
  deploy, so revalidate it instead of letting it fall through uncached.
@njbrake
njbrake deployed to fork-preview-approval August 24, 2026 15:10 — with GitHub Actions Active
@njbrake
njbrake deployed to fork-preview-approval August 24, 2026 19:41 — with GitHub Actions Active
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.

2 participants