Skip to content
Draft
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 9 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,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
25 changes: 25 additions & 0 deletions demos/react-supabase-time-based-sync/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
121 changes: 121 additions & 0 deletions demos/react-supabase-time-based-sync/README.md
Original file line number Diff line number Diff line change
@@ -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)
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.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"
}
}
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
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.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
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) = subscription.parameter('date')
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