Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions demos/react-supabase-time-based-sync/.env.local.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 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
27 changes: 27 additions & 0 deletions demos/react-supabase-time-based-sync/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 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
# don't ignore env file in powersync/docker directory
!powersync/docker/.env

# typescript
*.tsbuildinfo

# IDE
.vscode
.fleet
.idea

ios/
android/
144 changes: 144 additions & 0 deletions demos/react-supabase-time-based-sync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# 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. The backend contains a set of issues with `created_at` / `updated_at` as **`TIMESTAMPTZ`** in Postgres. The client passes the selected **UTC calendar dates** (`YYYY-MM-DD`) as a JSON array to a single sync stream subscription. Toggling dates on or off updates the array 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` and uses `json_each()` to expand the array parameter:

```yaml
streams:
issues_by_date:
query: |
SELECT * FROM issues
WHERE substring(updated_at, 1, 10) IN (SELECT value FROM json_each(subscription.parameter('dates')))
```

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:

1. **Filters the local query** with the same predicate as the stream (`substring(updated_at, 1, 10)` plus bound `?` placeholders), or `WHERE 1 = 0` when no dates are selected.
2. **Subscribes via `useSyncStream` in a small child** that only mounts when at least one date is selected (`ttl: 0` matches immediate eviction when nothing is selected).
3. **Does not pass `streams` into `useQuery`** — doing so resets internal “stream synced” state on every parameter change and briefly clears `data`, which flickers the list when toggling chips quickly.

```tsx
import { useQuery, useSyncStream } from '@powersync/react';

function IssuesByDateStreamSubscription({ datesParam }: { datesParam: string }) {
useSyncStream({ name: 'issues_by_date', parameters: { dates: datesParam }, ttl: 0 });
return null;
}

// Inside your page component:
const datesParam = React.useMemo(() => JSON.stringify(selectedDates), [selectedDates]);

const { issuesSql, issuesParams } = React.useMemo(() => {
if (selectedDates.length === 0) {
return {
issuesSql: `SELECT * FROM issues WHERE 1 = 0 ORDER BY created_at DESC`,
issuesParams: [] as string[]
};
}
const placeholders = selectedDates.map(() => '?').join(', ');
return {
issuesSql: `SELECT * FROM issues WHERE substring(updated_at, 1, 10) IN (${placeholders}) ORDER BY created_at DESC`,
issuesParams: selectedDates
};
}, [selectedDates]);

const { data: issues } = useQuery(issuesSql, issuesParams);

return (
<>
{selectedDates.length > 0 ? (
<IssuesByDateStreamSubscription datesParam={datesParam} />
) : null}
{/* …render chips and list from `issues`… */}
</>
);
```

In the repo, those `SELECT * FROM issues` fragments use `` `${ISSUES_TABLE}` `` from `src/library/powersync/AppSchema.ts` (the table name is `'issues'`), and the hook is `useQuery<IssueRecord>(issuesSql, issuesParams)`.

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:

```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)
37 changes: 37 additions & 0 deletions demos/react-supabase-time-based-sync/package.json
Original file line number Diff line number Diff line change
@@ -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.0",
"@powersync/web": "^1.34.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"
}
}
2 changes: 2 additions & 0 deletions demos/react-supabase-time-based-sync/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
packages:
- .
6 changes: 6 additions & 0 deletions demos/react-supabase-time-based-sync/powersync/cli.yaml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions demos/react-supabase-time-based-sync/powersync/docker/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
PS_PORT=8080
PS_DATA_SOURCE_URI=postgresql://postgres:postgres@supabase_db_react-supabase-time-based-sync:5432/postgres
PS_STORAGE_SOURCE_URI=postgresql://postgres:postgres@supabase_db_react-supabase-time-based-sync:5432/postgres
Original file line number Diff line number Diff line change
@@ -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
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
31 changes: 31 additions & 0 deletions demos/react-supabase-time-based-sync/powersync/service.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
config:
edition: 3

streams:
issues_by_date:
query: |
SELECT * FROM issues
WHERE substring(updated_at, 1, 10) IN (SELECT value FROM json_each(subscription.parameter('dates')))
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions demos/react-supabase-time-based-sync/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -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));
}
18 changes: 18 additions & 0 deletions demos/react-supabase-time-based-sync/src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -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(<App />);

export function App() {
return (
<ThemeProviderContainer>
<SystemProvider>
<RouterProvider router={router} />
</SystemProvider>
</ThemeProviderContainer>
);
}
26 changes: 26 additions & 0 deletions demos/react-supabase-time-based-sync/src/app/router.tsx
Original file line number Diff line number Diff line change
@@ -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: <Navigate to={DEFAULT_ENTRY_ROUTE} replace />
},
{
element: (
<ViewsLayout>
<Outlet />
</ViewsLayout>
),
children: [
{
path: ISSUES_ROUTE,
element: <IssuesPage />
}
]
}
]);
Loading
Loading