diff --git a/README.md b/README.md
index f37d9cda3..3147f0a63 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,7 @@ Demo applications are located in the [`demos/`](./demos/) directory. Also see ou
- [demos/react-supabase-todolist](./demos/react-supabase-todolist/README.md): A React to-do list example app using the PowerSync Web SDK and a Supabase backend.
- [demos/react-supabase-todolist-tanstackdb](./demos/react-supabase-todolist-tanstackdb/README.md): A React to-do list example app using the PowerSync Web SDK and a Supabase backend + [TanStackDB](https://tanstack.com/db/latest) collections.
+- [demos/react-supabase-time-based-sync](./demos/react-supabase-time-based-sync/README.md): A React demo using Sync Streams to subscribe to date-filtered data dynamically, with a Supabase backend.
- [demos/react-multi-client](./demos/react-multi-client/README.md): A React widget that illustrates how data flows from one PowerSync client to another.
- [demos/yjs-react-supabase-text-collab](./demos/yjs-react-supabase-text-collab/README.md): A React real-time text editing collaboration example app powered by [Yjs](https://github.com/yjs/yjs) CRDTs and [Tiptap](https://tiptap.dev/), using the PowerSync Web SDK and a Supabase backend.
- [demos/vue-supabase-todolist](./demos/vue-supabase-todolist/README.md): A Vue to-do list example app using the PowerSync Web SDK and a Supabase backend.
diff --git a/demos/react-supabase-time-based-sync/.env.local.template b/demos/react-supabase-time-based-sync/.env.local.template
new file mode 100644
index 000000000..546d659d8
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/.env.local.template
@@ -0,0 +1,9 @@
+# Copy this template: `cp .env.local.template .env.local`
+# Values below point to local Supabase + local PowerSync.
+# The anon key is the well-known default for all local Supabase instances.
+VITE_SUPABASE_URL=http://127.0.0.1:54321
+VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
+VITE_POWERSYNC_URL=http://127.0.0.1:8080
+
+# PowerSync Service port
+PS_PORT=8080
diff --git a/demos/react-supabase-time-based-sync/.gitignore b/demos/react-supabase-time-based-sync/.gitignore
new file mode 100644
index 000000000..2101efeea
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/.gitignore
@@ -0,0 +1,25 @@
+# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
+
+# dependencies
+node_modules/
+
+# Metrom
+.metro-health-check*
+
+# debug
+npm-debug.*
+
+# local env files
+.env*.local
+.env
+
+# typescript
+*.tsbuildinfo
+
+# IDE
+.vscode
+.fleet
+.idea
+
+ios/
+android/
\ No newline at end of file
diff --git a/demos/react-supabase-time-based-sync/README.md b/demos/react-supabase-time-based-sync/README.md
new file mode 100644
index 000000000..d88ab0793
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/README.md
@@ -0,0 +1,121 @@
+# PowerSync + Supabase: Time-Based Sync (Local-First)
+
+This demo shows how to use [PowerSync Sync Streams](https://docs.powersync.com/sync/sync-streams) to dynamically control which data is synced to the client based on a date. The backend contains a set of issues with `created_at` / `updated_at` as **`TIMESTAMPTZ`** in Postgres. Each selected date creates its own sync stream subscription with a `date` parameter. Toggling dates on or off adds or removes stream subscriptions and PowerSync syncs the matching issues. TTL is set to 0 so data is removed immediately when dates are deselected.
+
+This lets you model patterns like “sync the last N days of data” or “sync only the time ranges the user cares about” without re-deploying sync rules.
+
+The stream definition lives in `powersync/sync-config.yaml`:
+
+```yaml
+streams:
+ issues_by_date:
+ query: |
+ SELECT * FROM issues
+ WHERE substring(updated_at, 1, 10) = subscription.parameter('date')
+```
+
+Postgres `TIMESTAMPTZ` values are handled like text for the first 10 characters (the `YYYY-MM-DD` prefix) in both the sync stream query and on the client replica.
+
+The client implementation is in `src/app/views/issues/page.tsx`. It builds an array of stream options from the selected dates and passes them directly to `useQuery` via the `streams` option:
+
+```tsx
+import { useQuery } from '@powersync/react';
+
+const streams = selectedDates.map((date) => ({
+ name: 'issues_by_date',
+ parameters: { date },
+ ttl: 0
+}));
+
+const { data: issues } = useQuery(
+ 'SELECT * FROM issues ORDER BY updated_at DESC',
+ [],
+ { streams }
+);
+```
+
+`useQuery` manages the stream subscriptions internally — subscribing to new streams and unsubscribing from removed ones as the array changes.
+
+The demo runs against local Supabase (`supabase start`) and self-hosted PowerSync (via the PowerSync CLI). It uses anonymous Supabase auth — there is no login or registration flow.
+
+## Prerequisites
+
+- [Docker](https://docs.docker.com/get-docker/) (running)
+- [Supabase CLI](https://supabase.com/docs/guides/local-development/cli/getting-started)
+- [PowerSync CLI](https://docs.powersync.com/tools/cli)
+
+## Local development (recommended)
+
+1. Switch into this demo:
+
+ ```bash
+ cd demos/react-supabase-time-based-sync
+ ```
+
+2. Install dependencies:
+
+ ```bash
+ pnpm install
+ ```
+
+3. Create env file:
+
+ ```bash
+ cp .env.local.template .env.local
+ ```
+
+ The template already contains the well-known local Supabase anon key, so no manual changes are needed.
+
+4. Start local Supabase + local PowerSync:
+
+ > Ensure the [PowerSync CLI](https://docs.powersync.com/tools/cli) is installed before running the following command.
+
+ ```bash
+ pnpm local:up
+ ```
+
+ This does three things:
+ - starts Supabase Docker services
+ - starts PowerSync using the checked-in `powersync/service.yaml`
+ - loads sync streams from `powersync/sync-config.yaml`
+
+5. Start the app:
+
+ ```bash
+ pnpm dev
+ ```
+
+Open [http://localhost:5173](http://localhost:5173).
+
+## Database setup and seed data
+
+The schema and seed data are in `supabase/migrations/20260312000000_init_issues.sql`.
+
+When Supabase starts for the first time, the migration creates:
+
+- the `issues` table (`created_at` / `updated_at` are `TIMESTAMPTZ`)
+- RLS policies for authenticated users (including anonymous sessions)
+- realtime publication for `issues`
+- sample issues used by the time-based sync filters
+
+Run `supabase db reset` to re-apply migrations from scratch (required if you previously applied this migration when `created_at` / `updated_at` were `TEXT`).
+
+```bash
+supabase db reset
+```
+
+## Notes
+
+- The app signs in with `signInAnonymously()` automatically in the connector.
+- No login/register routes are used in this demo.
+- To stop local services:
+
+ ```bash
+ pnpm local:down
+ ```
+
+## Learn More
+
+- [PowerSync CLI docs](https://docs.powersync.com/tools/cli)
+- [PowerSync Sync Streams](https://docs.powersync.com/sync/sync-streams)
+- [Supabase anonymous sign-ins](https://supabase.com/docs/guides/auth/auth-anonymous)
diff --git a/demos/react-supabase-time-based-sync/package.json b/demos/react-supabase-time-based-sync/package.json
new file mode 100644
index 000000000..3b2c6fa67
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "react-supabase-time-based-sync",
+ "version": "0.1.0",
+ "private": true,
+ "description": "PowerSync React demo for time-based sync using sync streams (edition 3)",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.node.json && vite build",
+ "preview": "vite preview",
+ "start": "pnpm build && pnpm preview",
+ "local:up": "supabase start && powersync docker start",
+ "local:down": "powersync docker stop && supabase stop"
+ },
+ "dependencies": {
+ "@powersync/react": "^1.9.1",
+ "@powersync/web": "^1.37.0",
+ "@emotion/react": "11.11.4",
+ "@emotion/styled": "11.11.5",
+ "@journeyapps/wa-sqlite": "^1.5.0",
+ "@mui/icons-material": "^5.15.12",
+ "@mui/material": "^5.15.12",
+ "@supabase/supabase-js": "^2.39.7",
+ "formik": "^2.4.6",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.22.3"
+ },
+ "devDependencies": {
+ "@swc/core": "~1.6.0",
+ "@types/node": "^20.11.25",
+ "@types/react": "^18.2.64",
+ "@types/react-dom": "^18.2.21",
+ "@vitejs/plugin-react": "^4.2.1",
+ "typescript": "^5.4.2",
+ "vite": "^5.1.5"
+ }
+}
diff --git a/demos/react-supabase-time-based-sync/pnpm-workspace.yaml b/demos/react-supabase-time-based-sync/pnpm-workspace.yaml
new file mode 100644
index 000000000..d05a7e7dc
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/pnpm-workspace.yaml
@@ -0,0 +1,2 @@
+packages:
+ - .
diff --git a/demos/react-supabase-time-based-sync/powersync/cli.yaml b/demos/react-supabase-time-based-sync/powersync/cli.yaml
new file mode 100644
index 000000000..28e44add0
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/powersync/cli.yaml
@@ -0,0 +1,6 @@
+type: self-hosted
+api_url: http://localhost:8080
+api_key: dev-token
+plugins:
+ docker:
+ project_name: powersync_react-supabase-time-based-sync
diff --git a/demos/react-supabase-time-based-sync/powersync/docker/docker-compose.yaml b/demos/react-supabase-time-based-sync/powersync/docker/docker-compose.yaml
new file mode 100644
index 000000000..701c38711
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/powersync/docker/docker-compose.yaml
@@ -0,0 +1,35 @@
+# Composed PowerSync Docker stack (generated by powersync docker configure).
+# Modules add entries to include and to services.powersync.depends_on.
+# Relative paths: . = powersync/docker, .. = powersync.
+# Include syntax requires Docker Compose v2.20.3+
+
+include: []
+
+services:
+ powersync:
+ restart: unless-stopped
+ image: journeyapps/powersync-service:latest
+ command: [ 'start', '-r', 'unified' ]
+ env_file:
+ - ../../.env.local
+ volumes:
+ - ../service.yaml:/config/service.yaml
+ - ../sync-config.yaml:/config/sync-config.yaml
+ environment:
+ POWERSYNC_CONFIG_PATH: /config/service.yaml
+ NODE_OPTIONS: --max-old-space-size=1000
+ healthcheck:
+ test:
+ - 'CMD'
+ - 'node'
+ - '-e'
+ - "fetch('http://localhost:${PS_PORT:-8080}/probes/liveness').then(r =>
+ r.ok ? process.exit(0) : process.exit(1)).catch(() =>
+ process.exit(1))"
+ interval: 5s
+ timeout: 1s
+ retries: 15
+ ports:
+ - '${PS_PORT:-8080}:${PS_PORT:-8080}'
+ depends_on: {}
+name: powersync_react-supabase-time-based-sync
diff --git a/demos/react-supabase-time-based-sync/powersync/service.yaml b/demos/react-supabase-time-based-sync/powersync/service.yaml
new file mode 100644
index 000000000..eabe18f46
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/powersync/service.yaml
@@ -0,0 +1,31 @@
+# yaml-language-server: $schema=https://unpkg.com/@powersync/service-schema@latest/json-schema/powersync-config.json
+_type: self-hosted
+
+replication:
+ connections:
+ - type: postgresql
+ uri: postgresql://postgres:postgres@host.docker.internal:54322/postgres
+ sslmode: disable
+
+storage:
+ type: postgresql
+ uri: postgresql://postgres:postgres@host.docker.internal:54322/postgres
+ sslmode: disable
+
+sync_config:
+ path: ./sync-config.yaml
+
+port: 8080
+
+client_auth:
+ jwks_uri: http://host.docker.internal:54321/auth/v1/.well-known/jwks.json
+ audience:
+ - authenticated
+
+telemetry:
+ prometheus_port: 9090
+ disable_telemetry_sharing: true
+
+api:
+ tokens:
+ - dev-token
diff --git a/demos/react-supabase-time-based-sync/powersync/sync-config.yaml b/demos/react-supabase-time-based-sync/powersync/sync-config.yaml
new file mode 100644
index 000000000..9d3378e1c
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/powersync/sync-config.yaml
@@ -0,0 +1,8 @@
+config:
+ edition: 3
+
+streams:
+ issues_by_date:
+ query: |
+ SELECT * FROM issues
+ WHERE substring(updated_at, 1, 10) = subscription.parameter('date')
diff --git a/demos/react-supabase-time-based-sync/public/favicon.ico b/demos/react-supabase-time-based-sync/public/favicon.ico
new file mode 100644
index 000000000..918ca54ee
Binary files /dev/null and b/demos/react-supabase-time-based-sync/public/favicon.ico differ
diff --git a/demos/react-supabase-time-based-sync/public/icons/icon-192x192.png b/demos/react-supabase-time-based-sync/public/icons/icon-192x192.png
new file mode 100644
index 000000000..66a723429
Binary files /dev/null and b/demos/react-supabase-time-based-sync/public/icons/icon-192x192.png differ
diff --git a/demos/react-supabase-time-based-sync/public/icons/icon-256x256.png b/demos/react-supabase-time-based-sync/public/icons/icon-256x256.png
new file mode 100644
index 000000000..1b8b97bae
Binary files /dev/null and b/demos/react-supabase-time-based-sync/public/icons/icon-256x256.png differ
diff --git a/demos/react-supabase-time-based-sync/public/icons/icon-384x384.png b/demos/react-supabase-time-based-sync/public/icons/icon-384x384.png
new file mode 100644
index 000000000..af8be4dc6
Binary files /dev/null and b/demos/react-supabase-time-based-sync/public/icons/icon-384x384.png differ
diff --git a/demos/react-supabase-time-based-sync/public/icons/icon-512x512.png b/demos/react-supabase-time-based-sync/public/icons/icon-512x512.png
new file mode 100644
index 000000000..eb291c7e4
Binary files /dev/null and b/demos/react-supabase-time-based-sync/public/icons/icon-512x512.png differ
diff --git a/demos/react-supabase-time-based-sync/public/icons/icon.png b/demos/react-supabase-time-based-sync/public/icons/icon.png
new file mode 100644
index 000000000..c254b17c6
Binary files /dev/null and b/demos/react-supabase-time-based-sync/public/icons/icon.png differ
diff --git a/demos/react-supabase-time-based-sync/public/powersync-logo.svg b/demos/react-supabase-time-based-sync/public/powersync-logo.svg
new file mode 100644
index 000000000..05e31b6ed
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/public/powersync-logo.svg
@@ -0,0 +1 @@
+
diff --git a/demos/react-supabase-time-based-sync/public/supabase-logo.png b/demos/react-supabase-time-based-sync/public/supabase-logo.png
new file mode 100644
index 000000000..ff8c18e18
Binary files /dev/null and b/demos/react-supabase-time-based-sync/public/supabase-logo.png differ
diff --git a/demos/react-supabase-time-based-sync/src/app/globals.css b/demos/react-supabase-time-based-sync/src/app/globals.css
new file mode 100644
index 000000000..5ceb26045
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/src/app/globals.css
@@ -0,0 +1,12 @@
+:root {
+ --foreground-rgb: 255, 255, 255;
+ --background-start-rgb: 0, 0, 0;
+ --background-end-rgb: 0, 0, 0;
+}
+
+body {
+ color: rgb(var(--foreground-rgb));
+ min-height: 100vh;
+ margin: 0;
+ background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
+}
diff --git a/demos/react-supabase-time-based-sync/src/app/index.tsx b/demos/react-supabase-time-based-sync/src/app/index.tsx
new file mode 100644
index 000000000..e6aa8fae0
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/src/app/index.tsx
@@ -0,0 +1,18 @@
+import { createRoot } from 'react-dom/client';
+import { RouterProvider } from 'react-router-dom';
+import { SystemProvider } from '@/components/providers/SystemProvider';
+import { ThemeProviderContainer } from '@/components/providers/ThemeProviderContainer';
+import { router } from '@/app/router';
+
+const root = createRoot(document.getElementById('app')!);
+root.render();
+
+export function App() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/demos/react-supabase-time-based-sync/src/app/router.tsx b/demos/react-supabase-time-based-sync/src/app/router.tsx
new file mode 100644
index 000000000..a4ecdba3b
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/src/app/router.tsx
@@ -0,0 +1,26 @@
+import { Outlet, createBrowserRouter, Navigate } from 'react-router-dom';
+import IssuesPage from '@/app/views/issues/page';
+import ViewsLayout from '@/app/views/layout';
+
+export const ISSUES_ROUTE = '/views/issues';
+export const DEFAULT_ENTRY_ROUTE = ISSUES_ROUTE;
+
+export const router = createBrowserRouter([
+ {
+ path: '/',
+ element:
+ },
+ {
+ element: (
+
+
+
+ ),
+ children: [
+ {
+ path: ISSUES_ROUTE,
+ element:
+ }
+ ]
+ }
+]);
diff --git a/demos/react-supabase-time-based-sync/src/app/views/issues/page.tsx b/demos/react-supabase-time-based-sync/src/app/views/issues/page.tsx
new file mode 100644
index 000000000..62a2ca6b3
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/src/app/views/issues/page.tsx
@@ -0,0 +1,79 @@
+import { NavigationPage } from '@/components/navigation/NavigationPage';
+import { IssueItemWidget } from '@/components/widgets/IssueItemWidget';
+import { ISSUES_TABLE, IssueRecord } from '@/library/powersync/AppSchema';
+import { Box, Chip, List, Stack, Typography, styled } from '@mui/material';
+import { useQuery } from '@powersync/react';
+import React from 'react';
+
+const AVAILABLE_DATES = ['2026-01-15', '2026-01-14', '2026-01-10', '2026-01-07', '2026-01-05'];
+
+export default function IssuesPage() {
+ const [selectedDates, setSelectedDates] = React.useState(AVAILABLE_DATES);
+
+ const toggleDate = (date: string) => {
+ setSelectedDates((prev) =>
+ prev.includes(date) ? prev.filter((d) => d !== date) : [...prev, date].sort().reverse()
+ );
+ };
+
+ const streams = selectedDates.map((date) => ({ name: 'issues_by_date', parameters: { date }, ttl: 0 }));
+
+ const { data: issues } = useQuery(
+ `SELECT * FROM ${ISSUES_TABLE} ORDER BY updated_at DESC`,
+ [],
+ { streams }
+ );
+
+ return (
+
+
+
+
+ Each selected date subscribes to its own sync stream. Deselecting a date unsubscribes
+ from that stream and local data is removed (TTL = 0).
+
+
+ Syncing dates:
+
+
+ {AVAILABLE_DATES.map((date) => (
+ toggleDate(date)}
+ variant={selectedDates.includes(date) ? 'filled' : 'outlined'}
+ />
+ ))}
+
+
+ {selectedDates.length} date{selectedDates.length !== 1 ? 's' : ''} selected
+
+
+
+
+ {issues.length === 0 ? (
+
+ No issues found. Toggle dates above to sync issues.
+
+ ) : (
+
+ {issues.map((issue) => (
+
+ ))}
+
+ )}
+
+
+
+ );
+}
+
+namespace S {
+ export const ControlsBox = styled(Box)`
+ padding: 16px;
+ margin-bottom: 16px;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.05);
+ `;
+}
diff --git a/demos/react-supabase-time-based-sync/src/app/views/layout.tsx b/demos/react-supabase-time-based-sync/src/app/views/layout.tsx
new file mode 100644
index 000000000..d56ea535a
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/src/app/views/layout.tsx
@@ -0,0 +1,149 @@
+import { ISSUES_ROUTE } from '@/app/router';
+import { useNavigationPanel } from '@/components/navigation/NavigationPanelContext';
+import { useSupabase } from '@/components/providers/SystemProvider';
+import BugReportIcon from '@mui/icons-material/BugReport';
+import MenuIcon from '@mui/icons-material/Menu';
+import NorthIcon from '@mui/icons-material/North';
+import SignalWifiOffIcon from '@mui/icons-material/SignalWifiOff';
+import SouthIcon from '@mui/icons-material/South';
+import WifiIcon from '@mui/icons-material/Wifi';
+import {
+ AppBar,
+ Box,
+ Divider,
+ Drawer,
+ IconButton,
+ List,
+ ListItem,
+ ListItemButton,
+ ListItemIcon,
+ ListItemText,
+ Menu,
+ MenuItem,
+ Toolbar,
+ Typography,
+ styled
+} from '@mui/material';
+import { usePowerSync, useStatus } from '@powersync/react';
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
+
+type NavigationItem = {
+ path: string;
+ title: string;
+ icon: () => JSX.Element;
+ beforeNavigate?: () => Promise;
+};
+
+export default function ViewsLayout({ children }: { children: React.ReactNode }) {
+ const powerSync = usePowerSync();
+ const status = useStatus();
+ const supabase = useSupabase();
+ const navigate = useNavigate();
+
+ const [openDrawer, setOpenDrawer] = React.useState(false);
+ const { title } = useNavigationPanel();
+
+ const [connectionAnchor, setConnectionAnchor] = React.useState(null);
+
+ const NAVIGATION_ITEMS: NavigationItem[] = React.useMemo(
+ () => [
+ {
+ path: ISSUES_ROUTE,
+ title: 'Issues',
+ icon: () =>
+ }
+ ],
+ []
+ );
+
+ return (
+
+
+
+ setOpenDrawer(!openDrawer)}>
+
+
+
+ {title}
+
+
+
+ {
+ setConnectionAnchor(event.currentTarget);
+ }}>
+ {status?.connected ? : }
+
+
+
+
+ setOpenDrawer(false)}>
+
+
+
+ {NAVIGATION_ITEMS.map((item) => (
+
+ {
+ await item.beforeNavigate?.();
+ navigate(item.path);
+ setOpenDrawer(false);
+ }}>
+ {item.icon()}
+
+
+
+ ))}
+
+
+ {children}
+
+ );
+}
+
+namespace S {
+ export const MainBox = styled(Box)`
+ flex-grow: 1;
+ `;
+
+ export const TopBar = styled(AppBar)`
+ margin-bottom: 20px;
+ `;
+
+ export const PowerSyncLogo = styled('img')`
+ max-width: 250px;
+ max-height: 250px;
+ object-fit: contain;
+ padding: 20px;
+ `;
+}
diff --git a/demos/react-supabase-time-based-sync/src/components/navigation/NavigationPage.tsx b/demos/react-supabase-time-based-sync/src/components/navigation/NavigationPage.tsx
new file mode 100644
index 000000000..ac0cc3072
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/src/components/navigation/NavigationPage.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { useNavigationPanel } from './NavigationPanelContext';
+import { Box, styled } from '@mui/material';
+
+/**
+ * Wraps a component with automatic navigation panel title management
+ */
+export const NavigationPage: React.FC> = ({ title, children }) => {
+ const navigationPanel = useNavigationPanel();
+
+ React.useEffect(() => {
+ navigationPanel.setTitle(title);
+
+ return () => navigationPanel.setTitle('');
+ }, [title, navigationPanel]);
+
+ return {children};
+};
+
+namespace S {
+ export const Container = styled(Box)`
+ margin: 10px;
+ `;
+}
diff --git a/demos/react-supabase-time-based-sync/src/components/navigation/NavigationPanelContext.tsx b/demos/react-supabase-time-based-sync/src/components/navigation/NavigationPanelContext.tsx
new file mode 100644
index 000000000..4745475d3
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/src/components/navigation/NavigationPanelContext.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+
+export type NavigationPanelController = {
+ setTitle: (title: string) => void;
+ title: string;
+};
+
+export const NavigationPanelContext = React.createContext({
+ setTitle: () => {
+ throw new Error(`No NavigationPanelContext has been provided`);
+ },
+ title: ''
+});
+
+export const NavigationPanelContextProvider = ({ children }: { children: React.ReactNode }) => {
+ const [title, setTitle] = React.useState('');
+
+ return {children};
+};
+
+export const useNavigationPanel = () => React.useContext(NavigationPanelContext);
diff --git a/demos/react-supabase-time-based-sync/src/components/providers/SystemProvider.tsx b/demos/react-supabase-time-based-sync/src/components/providers/SystemProvider.tsx
new file mode 100644
index 000000000..618fd172f
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/src/components/providers/SystemProvider.tsx
@@ -0,0 +1,54 @@
+import { AppSchema } from '@/library/powersync/AppSchema';
+import { SupabaseConnector } from '@/library/powersync/SupabaseConnector';
+import { CircularProgress } from '@mui/material';
+import { PowerSyncContext } from '@powersync/react';
+import { createBaseLogger, LogLevel, PowerSyncDatabase, SyncClientImplementation } from '@powersync/web';
+import React, { Suspense } from 'react';
+import { NavigationPanelContextProvider } from '../navigation/NavigationPanelContext';
+
+const SupabaseContext = React.createContext(null);
+export const useSupabase = () => React.useContext(SupabaseContext);
+
+export const db = new PowerSyncDatabase({
+ schema: AppSchema,
+ database: {
+ dbFilename: 'time.db'
+ }
+});
+
+export const SystemProvider = ({ children }: { children: React.ReactNode }) => {
+ const [connector] = React.useState(() => new SupabaseConnector());
+ const [powerSync] = React.useState(db);
+
+ React.useEffect(() => {
+ const logger = createBaseLogger();
+ logger.useDefaults();
+ logger.setLevel(LogLevel.DEBUG);
+ // For console testing purposes
+ (window as any)._powersync = powerSync;
+
+ powerSync.init();
+ const l = connector.registerListener({
+ initialized: () => {},
+ sessionStarted: () => {
+ powerSync.connect(connector, { clientImplementation: SyncClientImplementation.RUST });
+ }
+ });
+
+ connector.init();
+
+ return () => l?.();
+ }, [powerSync, connector]);
+
+ return (
+ }>
+
+
+ {children}
+
+
+
+ );
+};
+
+export default SystemProvider;
diff --git a/demos/react-supabase-time-based-sync/src/components/providers/ThemeProviderContainer.tsx b/demos/react-supabase-time-based-sync/src/components/providers/ThemeProviderContainer.tsx
new file mode 100644
index 000000000..9b9c1d61f
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/src/components/providers/ThemeProviderContainer.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { createTheme, ThemeProvider } from '@mui/material/styles';
+
+export const ThemeProviderContainer: React.FC> = ({ children }) => {
+ const theme = React.useMemo(() => {
+ return createTheme({
+ palette: {
+ mode: 'dark',
+ primary: {
+ main: '#c44eff'
+ }
+ },
+ typography: {
+ fontFamily: 'Rubik, sans-serif'
+ }
+ });
+ }, []);
+
+ return {children};
+};
diff --git a/demos/react-supabase-time-based-sync/src/components/widgets/IssueItemWidget.tsx b/demos/react-supabase-time-based-sync/src/components/widgets/IssueItemWidget.tsx
new file mode 100644
index 000000000..1421cc1b1
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/src/components/widgets/IssueItemWidget.tsx
@@ -0,0 +1,86 @@
+import { ISSUES_TABLE, IssueRecord } from '@/library/powersync/AppSchema';
+import DeleteIcon from '@mui/icons-material/DeleteOutline';
+import { Box, Chip, IconButton, ListItem, ListItemText, Paper, styled } from '@mui/material';
+import { usePowerSync } from '@powersync/react';
+import React from 'react';
+
+export type IssueItemWidgetProps = {
+ issue: IssueRecord;
+};
+
+const priorityColor = (priority: string | null): 'default' | 'info' | 'warning' | 'error' => {
+ switch (priority) {
+ case 'low':
+ return 'default';
+ case 'medium':
+ return 'info';
+ case 'high':
+ return 'warning';
+ case 'critical':
+ return 'error';
+ default:
+ return 'default';
+ }
+};
+
+const formatUpdatedAt = (value: string | null): string | null => {
+ if (value == null || value === '') return null;
+ const d = new Date(value);
+ return Number.isNaN(d.getTime()) ? value : d.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
+};
+
+const statusColor = (status: string | null): 'default' | 'success' | 'primary' => {
+ switch (status) {
+ case 'open':
+ return 'primary';
+ case 'closed':
+ return 'success';
+ default:
+ return 'default';
+ }
+};
+
+export const IssueItemWidget: React.FC = React.memo(({ issue }) => {
+ const powerSync = usePowerSync();
+
+ const deleteIssue = React.useCallback(async () => {
+ await powerSync.execute(`DELETE FROM ${ISSUES_TABLE} WHERE id = ?`, [issue.id]);
+ }, [issue.id]);
+
+ return (
+
+
+
+
+ }>
+
+ {issue.title}
+
+
+
+ }
+ secondary={
+
+ {issue.description}
+ {issue.updated_at && (
+
+ Updated: {formatUpdatedAt(issue.updated_at)}
+
+ )}
+
+ }
+ />
+
+
+ );
+});
+
+namespace S {
+ export const MainPaper = styled(Paper)`
+ margin-bottom: 10px;
+ `;
+}
diff --git a/demos/react-supabase-time-based-sync/src/index.html b/demos/react-supabase-time-based-sync/src/index.html
new file mode 100644
index 000000000..3bd908ca9
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/src/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/react-supabase-time-based-sync/src/library/powersync/AppSchema.ts b/demos/react-supabase-time-based-sync/src/library/powersync/AppSchema.ts
new file mode 100644
index 000000000..537a9db56
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/src/library/powersync/AppSchema.ts
@@ -0,0 +1,24 @@
+import { column, Schema, Table } from '@powersync/web';
+
+export const ISSUES_TABLE = 'issues';
+
+const issues = new Table(
+ {
+ title: column.text,
+ description: column.text,
+ status: column.text,
+ priority: column.text,
+ created_by: column.text,
+ // Postgres TIMESTAMPTZ — replicated to SQLite as text by PowerSync
+ created_at: column.text,
+ updated_at: column.text
+ },
+ { indexes: { updated: ['updated_at'] } }
+);
+
+export const AppSchema = new Schema({
+ issues
+});
+
+export type Database = (typeof AppSchema)['types'];
+export type IssueRecord = Database['issues'];
diff --git a/demos/react-supabase-time-based-sync/src/library/powersync/SupabaseConnector.ts b/demos/react-supabase-time-based-sync/src/library/powersync/SupabaseConnector.ts
new file mode 100644
index 000000000..5a1cadca3
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/src/library/powersync/SupabaseConnector.ts
@@ -0,0 +1,165 @@
+import {
+ AbstractPowerSyncDatabase,
+ BaseObserver,
+ CrudEntry,
+ PowerSyncBackendConnector,
+ UpdateType,
+ type PowerSyncCredentials
+} from '@powersync/web';
+
+import { Session, SupabaseClient, createClient } from '@supabase/supabase-js';
+
+export type SupabaseConfig = {
+ supabaseUrl: string;
+ supabaseAnonKey: string;
+ powersyncUrl: string;
+};
+
+/// Postgres Response codes that we cannot recover from by retrying.
+const FATAL_RESPONSE_CODES = [
+ // Class 22 — Data Exception
+ new RegExp('^22...$'),
+ // Class 23 — Integrity Constraint Violation
+ new RegExp('^23...$'),
+ // INSUFFICIENT PRIVILEGE - typically a row-level security violation
+ new RegExp('^42501$')
+];
+
+export type SupabaseConnectorListener = {
+ initialized: () => void;
+ sessionStarted: (session: Session) => void;
+};
+
+export class SupabaseConnector extends BaseObserver implements PowerSyncBackendConnector {
+ readonly client: SupabaseClient;
+ readonly config: SupabaseConfig;
+
+ ready: boolean;
+
+ currentSession: Session | null;
+
+ constructor() {
+ super();
+ this.config = {
+ supabaseUrl: import.meta.env.VITE_SUPABASE_URL,
+ powersyncUrl: import.meta.env.VITE_POWERSYNC_URL,
+ supabaseAnonKey: import.meta.env.VITE_SUPABASE_ANON_KEY
+ };
+
+ this.client = createClient(this.config.supabaseUrl, this.config.supabaseAnonKey, {
+ auth: {
+ persistSession: true
+ }
+ });
+ this.currentSession = null;
+ this.ready = false;
+ }
+
+ async init() {
+ if (this.ready) {
+ return;
+ }
+
+ let sessionResponse = await this.client.auth.getSession();
+ if (sessionResponse.error) {
+ throw sessionResponse.error;
+ }
+
+ if (!sessionResponse.data.session) {
+ const anonymousSignIn = await this.client.auth.signInAnonymously();
+ if (anonymousSignIn.error) {
+ throw anonymousSignIn.error;
+ }
+ sessionResponse = await this.client.auth.getSession();
+ if (sessionResponse.error) {
+ throw sessionResponse.error;
+ }
+ }
+
+ this.updateSession(sessionResponse.data.session);
+
+ this.ready = true;
+ this.iterateListeners((cb) => cb.initialized?.());
+ }
+
+ async fetchCredentials() {
+ let {
+ data: { session },
+ error
+ } = await this.client.auth.getSession();
+
+ if (!session && !error) {
+ const anonymousSignIn = await this.client.auth.signInAnonymously();
+ if (anonymousSignIn.error) {
+ throw anonymousSignIn.error;
+ }
+ const retry = await this.client.auth.getSession();
+ session = retry.data.session;
+ error = retry.error;
+ }
+
+ if (!session || error) {
+ throw new Error(`Could not fetch Supabase credentials: ${error}`);
+ }
+
+ console.debug('session expires at', session.expires_at);
+
+ return {
+ endpoint: this.config.powersyncUrl,
+ token: session.access_token ?? ''
+ } satisfies PowerSyncCredentials;
+ }
+
+ async uploadData(database: AbstractPowerSyncDatabase): Promise {
+ const transaction = await database.getNextCrudTransaction();
+
+ if (!transaction) {
+ return;
+ }
+
+ let lastOp: CrudEntry | null = null;
+ try {
+ for (const op of transaction.crud) {
+ lastOp = op;
+ const table = this.client.from(op.table);
+ let result: any;
+ switch (op.op) {
+ case UpdateType.PUT:
+ const record = { ...op.opData, id: op.id };
+ result = await table.upsert(record);
+ break;
+ case UpdateType.PATCH:
+ result = await table.update(op.opData).eq('id', op.id);
+ break;
+ case UpdateType.DELETE:
+ result = await table.delete().eq('id', op.id);
+ break;
+ }
+
+ if (result.error) {
+ console.error(result.error);
+ result.error.message = `Could not update Supabase. Received error: ${result.error.message}`;
+ throw result.error;
+ }
+ }
+
+ await transaction.complete();
+ } catch (ex: any) {
+ console.debug(ex);
+ if (typeof ex.code == 'string' && FATAL_RESPONSE_CODES.some((regex) => regex.test(ex.code))) {
+ console.error('Data upload error - discarding:', lastOp, ex);
+ await transaction.complete();
+ } else {
+ throw ex;
+ }
+ }
+ }
+
+ updateSession(session: Session | null) {
+ this.currentSession = session;
+ if (!session) {
+ return;
+ }
+ this.iterateListeners((cb) => cb.sessionStarted?.(session));
+ }
+}
diff --git a/demos/react-supabase-time-based-sync/src/library/powersync/vite-env.d.ts b/demos/react-supabase-time-based-sync/src/library/powersync/vite-env.d.ts
new file mode 100644
index 000000000..e3e71b5ba
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/src/library/powersync/vite-env.d.ts
@@ -0,0 +1,11 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_SUPABASE_URL: string;
+ readonly VITE_SUPABASE_ANON_KEY: string;
+ readonly VITE_POWERSYNC_URL: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/demos/react-supabase-time-based-sync/supabase/config.toml b/demos/react-supabase-time-based-sync/supabase/config.toml
new file mode 100644
index 000000000..45c22036e
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/supabase/config.toml
@@ -0,0 +1,85 @@
+# A string used to distinguish different Supabase projects on the same host.
+project_id = "react-supabase-time-based-sync"
+
+[api]
+enabled = true
+port = 54321
+schemas = ["public", "storage", "graphql_public"]
+extra_search_path = ["public", "extensions"]
+max_rows = 1000
+
+[db]
+port = 54322
+shadow_port = 54320
+major_version = 15
+
+[db.pooler]
+enabled = false
+port = 54329
+pool_mode = "transaction"
+default_pool_size = 20
+max_client_conn = 100
+
+[realtime]
+enabled = true
+
+[studio]
+enabled = true
+port = 54323
+api_url = "http://127.0.0.1"
+
+[inbucket]
+enabled = true
+port = 54324
+
+[storage]
+enabled = true
+file_size_limit = "50MiB"
+
+[auth]
+enabled = true
+site_url = "http://127.0.0.1:5173"
+additional_redirect_urls = ["http://127.0.0.1:5173"]
+jwt_expiry = 3600
+enable_refresh_token_rotation = true
+refresh_token_reuse_interval = 10
+enable_signup = true
+enable_anonymous_sign_ins = true
+
+[auth.email]
+enable_signup = true
+double_confirm_changes = true
+enable_confirmations = false
+
+[auth.sms]
+enable_signup = true
+enable_confirmations = false
+template = "Your code is {{ .Code }} ."
+
+[auth.sms.test_otp]
+
+[auth.sms.twilio]
+enabled = false
+account_sid = ""
+message_service_sid = ""
+auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
+
+[auth.external.apple]
+enabled = false
+client_id = ""
+secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
+redirect_uri = ""
+url = ""
+
+[analytics]
+enabled = false
+port = 54327
+vector_port = 54328
+backend = "postgres"
+
+[experimental]
+orioledb_version = ""
+s3_host = "env(S3_HOST)"
+s3_region = "env(S3_REGION)"
+s3_access_key = "env(S3_ACCESS_KEY)"
+s3_secret_key = "env(S3_SECRET_KEY)"
diff --git a/demos/react-supabase-time-based-sync/supabase/migrations/20260312000000_init_issues.sql b/demos/react-supabase-time-based-sync/supabase/migrations/20260312000000_init_issues.sql
new file mode 100644
index 000000000..c922085f5
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/supabase/migrations/20260312000000_init_issues.sql
@@ -0,0 +1,66 @@
+CREATE TABLE IF NOT EXISTS public.issues (
+ id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
+ title TEXT NOT NULL,
+ description TEXT,
+ status TEXT NOT NULL DEFAULT 'open',
+ priority TEXT NOT NULL DEFAULT 'medium',
+ created_by UUID REFERENCES auth.users(id),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+ALTER TABLE public.issues ENABLE ROW LEVEL SECURITY;
+
+DROP POLICY IF EXISTS "Authenticated users can read issues" ON public.issues;
+CREATE POLICY "Authenticated users can read issues"
+ ON public.issues FOR SELECT
+ TO authenticated
+ USING (true);
+
+DROP POLICY IF EXISTS "Authenticated users can insert issues" ON public.issues;
+CREATE POLICY "Authenticated users can insert issues"
+ ON public.issues FOR INSERT
+ TO authenticated
+ WITH CHECK (true);
+
+DROP POLICY IF EXISTS "Authenticated users can update issues" ON public.issues;
+CREATE POLICY "Authenticated users can update issues"
+ ON public.issues FOR UPDATE
+ TO authenticated
+ USING (true);
+
+DROP POLICY IF EXISTS "Authenticated users can delete issues" ON public.issues;
+CREATE POLICY "Authenticated users can delete issues"
+ ON public.issues FOR DELETE
+ TO authenticated
+ USING (true);
+
+DO
+$$
+BEGIN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM pg_publication_tables
+ WHERE pubname = 'supabase_realtime'
+ AND schemaname = 'public'
+ AND tablename = 'issues'
+ ) THEN
+ ALTER PUBLICATION supabase_realtime ADD TABLE public.issues;
+ END IF;
+END
+$$;
+
+INSERT INTO public.issues (title, description, status, priority, created_at, updated_at)
+VALUES
+ ('App crashes on login', 'Users report a crash when tapping the login button on iOS 17.', 'open', 'critical', timestamptz '2026-01-15 10:30:00+00', timestamptz '2026-01-15 10:30:00+00'),
+ ('Update onboarding copy', 'Marketing wants to revise the welcome screen text.', 'open', 'low', timestamptz '2026-01-15 14:00:00+00', timestamptz '2026-01-15 14:00:00+00'),
+ ('Fix broken pagination', 'The "next page" button returns an empty list after page 5.', 'open', 'high', timestamptz '2026-01-14 09:00:00+00', timestamptz '2026-01-14 09:00:00+00'),
+ ('Add dark mode toggle', 'Users have requested a dark mode option in settings.', 'open', 'medium', timestamptz '2026-01-14 11:30:00+00', timestamptz '2026-01-14 11:30:00+00'),
+ ('Database migration failing', 'Migration 042 fails on Postgres 15 due to deprecated syntax.', 'open', 'critical', timestamptz '2026-01-10 08:00:00+00', timestamptz '2026-01-10 08:00:00+00'),
+ ('Improve search performance', 'Search queries over 10k rows take >2s. Add proper indexing.', 'open', 'high', timestamptz '2026-01-10 16:45:00+00', timestamptz '2026-01-10 16:45:00+00'),
+ ('Typo in error message', 'Error says "Somthing went wrong" instead of "Something went wrong".', 'closed', 'low', timestamptz '2026-01-10 12:00:00+00', timestamptz '2026-01-10 12:00:00+00'),
+ ('Add CSV export', 'Allow users to export their data as CSV from the dashboard.', 'open', 'medium', timestamptz '2026-01-07 10:00:00+00', timestamptz '2026-01-07 10:00:00+00'),
+ ('Session timeout too short', 'Users are logged out after 5 minutes of inactivity. Increase to 30.', 'closed', 'high', timestamptz '2026-01-07 15:30:00+00', timestamptz '2026-01-07 15:30:00+00'),
+ ('Add rate limiting', 'API endpoints have no rate limiting. Add 100 req/min per user.', 'open', 'high', timestamptz '2026-01-05 09:15:00+00', timestamptz '2026-01-05 09:15:00+00'),
+ ('Fix timezone display', 'Times are shown in UTC instead of the user''s local timezone.', 'closed', 'medium', timestamptz '2026-01-05 13:00:00+00', timestamptz '2026-01-05 13:00:00+00')
+ON CONFLICT (id) DO NOTHING;
diff --git a/demos/react-supabase-time-based-sync/supabase/migrations/20260312001000_create_powersync_publication.sql b/demos/react-supabase-time-based-sync/supabase/migrations/20260312001000_create_powersync_publication.sql
new file mode 100644
index 000000000..1b1f9aeb8
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/supabase/migrations/20260312001000_create_powersync_publication.sql
@@ -0,0 +1,23 @@
+DO
+$$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_publication WHERE pubname = 'powersync') THEN
+ CREATE PUBLICATION powersync FOR ALL TABLES;
+ END IF;
+END
+$$;
+
+DO
+$$
+BEGIN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM pg_publication_tables
+ WHERE pubname = 'powersync'
+ AND schemaname = 'public'
+ AND tablename = 'issues'
+ ) THEN
+ ALTER PUBLICATION powersync ADD TABLE public.issues;
+ END IF;
+END
+$$;
diff --git a/demos/react-supabase-time-based-sync/tsconfig.json b/demos/react-supabase-time-based-sync/tsconfig.json
new file mode 100644
index 000000000..2a27e9673
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/demos/react-supabase-time-based-sync/tsconfig.node.json b/demos/react-supabase-time-based-sync/tsconfig.node.json
new file mode 100644
index 000000000..a8583534f
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.mts"]
+}
diff --git a/demos/react-supabase-time-based-sync/vite.config.mts b/demos/react-supabase-time-based-sync/vite.config.mts
new file mode 100644
index 000000000..fb5075fac
--- /dev/null
+++ b/demos/react-supabase-time-based-sync/vite.config.mts
@@ -0,0 +1,30 @@
+import { fileURLToPath, URL } from 'url';
+
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ root: 'src',
+ build: {
+ outDir: '../dist',
+ rollupOptions: {
+ input: 'src/index.html'
+ },
+ emptyOutDir: true
+ },
+ resolve: {
+ alias: [{ find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) }]
+ },
+ publicDir: '../public',
+ envDir: '..', // Use this dir for env vars, not 'src'.
+ optimizeDeps: {
+ // Don't optimize these packages as they contain web workers and WASM files.
+ // https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673
+ exclude: ['@powersync/web']
+ },
+ plugins: [react()],
+ worker: {
+ format: 'es'
+ }
+});