Skip to content

Commit 69afd29

Browse files
committed
feat(create): add React PowerSync scaffolding add-on
1 parent b8f4e63 commit 69afd29

10 files changed

Lines changed: 314 additions & 0 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# PowerSync
2+
3+
This project includes the PowerSync Web SDK and React hooks.
4+
5+
## Environment
6+
7+
Set these variables in `.env.local`:
8+
9+
- `VITE_POWERSYNC_URL`
10+
- `VITE_POWERSYNC_TOKEN` for local development only
11+
12+
## What The Add-on Includes
13+
14+
- `src/lib/powersync/AppSchema.ts`
15+
- `src/lib/powersync/BackendConnector.ts`
16+
- `src/integrations/powersync/provider.tsx`
17+
- `src/routes/demo/powersync.tsx`
18+
19+
## Next Steps
20+
21+
1. Replace the development token flow in `src/lib/powersync/BackendConnector.ts` with your real auth flow.
22+
2. Update the sample schema in `src/lib/powersync/AppSchema.ts` to match your synced tables.
23+
3. Implement the upload logic in `uploadData()` so local mutations are written back to your backend.
24+
25+
PowerSync setup guidance:
26+
https://docs.powersync.com/client-sdk-references/js-web
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
VITE_POWERSYNC_URL=
3+
VITE_POWERSYNC_TOKEN=
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Plugin } from 'vite'
2+
3+
export default function powersyncVite(): Plugin {
4+
return {
5+
name: 'powersync-vite',
6+
config() {
7+
return {
8+
optimizeDeps: {
9+
exclude: ['@powersync/web'],
10+
},
11+
worker: {
12+
format: 'es',
13+
},
14+
}
15+
},
16+
}
17+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { ReactNode } from 'react'
2+
import { PowerSyncContext } from '@powersync/react'
3+
import { PowerSyncDatabase, WASQLiteOpenFactory } from '@powersync/web'
4+
5+
import { AppSchema } from '#/lib/powersync/AppSchema'
6+
import { BackendConnector } from '#/lib/powersync/BackendConnector'
7+
8+
const db = new PowerSyncDatabase({
9+
database: new WASQLiteOpenFactory({
10+
dbFilename: 'powersync.db',
11+
}),
12+
schema: AppSchema,
13+
flags: {
14+
disableSSRWarning: true,
15+
},
16+
})
17+
18+
void db.connect(new BackendConnector())
19+
20+
export default function PowerSyncProvider({
21+
children,
22+
}: {
23+
children: ReactNode
24+
}) {
25+
return <PowerSyncContext.Provider value={db}>{children}</PowerSyncContext.Provider>
26+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Schema, Table, column } from '@powersync/web'
2+
3+
const todos = new Table(
4+
{
5+
created_at: column.text,
6+
description: column.text,
7+
completed: column.integer,
8+
},
9+
{ indexes: { created_at: ['created_at'] } },
10+
)
11+
12+
export const AppSchema = new Schema({
13+
todos,
14+
})
15+
16+
export type Database = (typeof AppSchema)['types']
17+
export type TodoRecord = Database['todos']
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
type AbstractPowerSyncDatabase,
3+
type PowerSyncBackendConnector,
4+
UpdateType,
5+
} from '@powersync/web'
6+
7+
export class BackendConnector implements PowerSyncBackendConnector {
8+
private readonly powersyncUrl = import.meta.env.VITE_POWERSYNC_URL
9+
private readonly powersyncToken = import.meta.env.VITE_POWERSYNC_TOKEN
10+
11+
async fetchCredentials() {
12+
if (!this.powersyncUrl || !this.powersyncToken) {
13+
return null
14+
}
15+
16+
return {
17+
endpoint: this.powersyncUrl,
18+
token: this.powersyncToken,
19+
}
20+
}
21+
22+
async uploadData(database: AbstractPowerSyncDatabase): Promise<void> {
23+
const transaction = await database.getNextCrudTransaction()
24+
25+
if (!transaction) {
26+
return
27+
}
28+
29+
try {
30+
for (const op of transaction.crud) {
31+
const record = { ...op.opData, id: op.id }
32+
33+
switch (op.op) {
34+
case UpdateType.PUT:
35+
console.info('TODO: create record remotely', record)
36+
break
37+
case UpdateType.PATCH:
38+
console.info('TODO: patch record remotely', record)
39+
break
40+
case UpdateType.DELETE:
41+
console.info('TODO: delete record remotely', record)
42+
break
43+
}
44+
}
45+
46+
await transaction.complete()
47+
} catch (error) {
48+
console.error('PowerSync uploadData failed', error)
49+
await transaction.complete()
50+
}
51+
}
52+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { useState } from 'react'
2+
import { createFileRoute } from '@tanstack/react-router'
3+
import { usePowerSync, useQuery, useStatus } from '@powersync/react'
4+
5+
export const Route = createFileRoute('/demo/powersync')({
6+
component: PowerSyncDemo,
7+
})
8+
9+
type TodoRow = {
10+
id: string
11+
created_at: string
12+
description: string
13+
completed: number
14+
}
15+
16+
function PowerSyncDemo() {
17+
const powerSync = usePowerSync()
18+
const status = useStatus()
19+
const { data } = useQuery(
20+
'SELECT id, created_at, description, completed FROM todos ORDER BY created_at DESC',
21+
)
22+
const todos = (data ?? []) as Array<TodoRow>
23+
const [description, setDescription] = useState('')
24+
25+
async function addTodo(event: React.FormEvent<HTMLFormElement>) {
26+
event.preventDefault()
27+
28+
const nextDescription = description.trim()
29+
if (!nextDescription) {
30+
return
31+
}
32+
33+
await powerSync.execute(
34+
'INSERT INTO todos (id, created_at, description, completed) VALUES (?, ?, ?, ?)',
35+
[crypto.randomUUID(), new Date().toISOString(), nextDescription, 0],
36+
)
37+
38+
setDescription('')
39+
}
40+
41+
return (
42+
<main className="page-wrap py-10">
43+
<div className="max-w-3xl space-y-6">
44+
<header className="space-y-2">
45+
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-[var(--sea-ink-soft)]">
46+
Offline Sync
47+
</p>
48+
<h1 className="text-3xl font-semibold tracking-tight">PowerSync</h1>
49+
<p className="text-sm text-[var(--sea-ink-soft)]">
50+
This demo writes to the local SQLite database immediately. Replace the sample
51+
schema and backend connector with your real PowerSync configuration.
52+
</p>
53+
</header>
54+
55+
<section className="rounded-3xl border border-[var(--line)] bg-white/70 p-5 shadow-sm">
56+
<h2 className="text-sm font-semibold uppercase tracking-[0.16em] text-[var(--sea-ink-soft)]">
57+
Connection State
58+
</h2>
59+
<pre className="mt-3 overflow-auto rounded-2xl bg-[var(--chip-bg)] p-4 text-xs leading-6 text-[var(--sea-ink)]">
60+
{JSON.stringify(status, null, 2)}
61+
</pre>
62+
</section>
63+
64+
<section className="rounded-3xl border border-[var(--line)] bg-white/70 p-5 shadow-sm">
65+
<h2 className="text-sm font-semibold uppercase tracking-[0.16em] text-[var(--sea-ink-soft)]">
66+
Local Todos
67+
</h2>
68+
<form className="mt-4 flex flex-col gap-3 sm:flex-row" onSubmit={addTodo}>
69+
<input
70+
className="min-w-0 flex-1 rounded-2xl border border-[var(--line)] bg-white px-4 py-3 text-sm text-[var(--sea-ink)] outline-none"
71+
onChange={(event) => setDescription(event.target.value)}
72+
placeholder="Write to the local PowerSync database"
73+
value={description}
74+
/>
75+
<button
76+
className="rounded-2xl bg-[var(--sea-ink)] px-4 py-3 text-sm font-semibold text-white"
77+
type="submit"
78+
>
79+
Insert Local Row
80+
</button>
81+
</form>
82+
83+
<ul className="mt-5 space-y-3">
84+
{todos.length === 0 ? (
85+
<li className="rounded-2xl border border-dashed border-[var(--line)] px-4 py-5 text-sm text-[var(--sea-ink-soft)]">
86+
No rows yet. Insert one locally, then wire `uploadData()` to send it upstream.
87+
</li>
88+
) : (
89+
todos.map((todo) => (
90+
<li
91+
className="rounded-2xl border border-[var(--line)] bg-[var(--chip-bg)] px-4 py-4"
92+
key={todo.id}
93+
>
94+
<div className="flex items-start justify-between gap-4">
95+
<div>
96+
<p className="font-medium text-[var(--sea-ink)]">{todo.description}</p>
97+
<p className="mt-1 text-xs text-[var(--sea-ink-soft)]">
98+
{todo.created_at}
99+
</p>
100+
</div>
101+
<span className="rounded-full border border-[var(--line)] px-2 py-1 text-xs font-semibold text-[var(--sea-ink-soft)]">
102+
{todo.completed ? 'done' : 'pending'}
103+
</span>
104+
</div>
105+
</li>
106+
))
107+
)}
108+
</ul>
109+
</section>
110+
</div>
111+
</main>
112+
)
113+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "PowerSync",
3+
"description": "Add PowerSync offline sync to your application.",
4+
"phase": "add-on",
5+
"type": "add-on",
6+
"category": "database",
7+
"color": "#2563EB",
8+
"priority": 55,
9+
"link": "https://docs.powersync.com/client-sdk-references/js-web",
10+
"modes": ["file-router"],
11+
"envVars": [
12+
{
13+
"name": "VITE_POWERSYNC_URL",
14+
"description": "PowerSync instance URL",
15+
"required": true,
16+
"file": ".env.local"
17+
},
18+
{
19+
"name": "VITE_POWERSYNC_TOKEN",
20+
"description": "Development token for local testing",
21+
"required": false,
22+
"secret": true,
23+
"file": ".env.local"
24+
}
25+
],
26+
"integrations": [
27+
{
28+
"type": "vite-plugin",
29+
"path": "powersync-vite-plugin.ts",
30+
"jsName": "powersyncVite",
31+
"code": "powersyncVite()"
32+
},
33+
{
34+
"type": "provider",
35+
"path": "src/integrations/powersync/provider.tsx",
36+
"jsName": "PowerSyncProvider"
37+
}
38+
],
39+
"routes": [
40+
{
41+
"url": "/demo/powersync",
42+
"name": "PowerSync",
43+
"path": "src/routes/demo/powersync.tsx",
44+
"jsName": "PowerSyncDemo"
45+
}
46+
]
47+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"dependencies": {
3+
"@journeyapps/wa-sqlite": "^1.2.6",
4+
"@powersync/react": "^1.7.4",
5+
"@powersync/web": "^1.26.1"
6+
}
7+
}
Lines changed: 6 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)