Skip to content

Commit 8f24af5

Browse files
authored
feat(create): add React PowerSync scaffolding add-on (#407)
* feat(create): add React PowerSync scaffolding add-on * fix(create): address PowerSync add-on review feedback
1 parent bc65360 commit 8f24af5

10 files changed

Lines changed: 329 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+
throw error
50+
}
51+
}
52+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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+
const [error, setError] = useState<string | null>(null)
25+
26+
async function addTodo(event: React.FormEvent<HTMLFormElement>) {
27+
event.preventDefault()
28+
29+
const nextDescription = description.trim()
30+
if (!nextDescription) {
31+
return
32+
}
33+
34+
try {
35+
setError(null)
36+
await powerSync.execute(
37+
'INSERT INTO todos (id, created_at, description, completed) VALUES (?, ?, ?, ?)',
38+
[crypto.randomUUID(), new Date().toISOString(), nextDescription, 0],
39+
)
40+
41+
setDescription('')
42+
} catch (error) {
43+
console.error('Failed to insert PowerSync todo', error)
44+
setError('Failed to insert row. Please try again.')
45+
}
46+
}
47+
48+
return (
49+
<main className="page-wrap py-10">
50+
<div className="max-w-3xl space-y-6">
51+
<header className="space-y-2">
52+
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-[var(--sea-ink-soft)]">
53+
Offline Sync
54+
</p>
55+
<h1 className="text-3xl font-semibold tracking-tight">PowerSync</h1>
56+
<p className="text-sm text-[var(--sea-ink-soft)]">
57+
This demo writes to the local SQLite database immediately. Replace the sample
58+
schema and backend connector with your real PowerSync configuration.
59+
</p>
60+
</header>
61+
62+
<section className="rounded-3xl border border-[var(--line)] bg-white/70 p-5 shadow-sm">
63+
<h2 className="text-sm font-semibold uppercase tracking-[0.16em] text-[var(--sea-ink-soft)]">
64+
Connection State
65+
</h2>
66+
<pre className="mt-3 overflow-auto rounded-2xl bg-[var(--chip-bg)] p-4 text-xs leading-6 text-[var(--sea-ink)]">
67+
{JSON.stringify(status, null, 2)}
68+
</pre>
69+
</section>
70+
71+
<section className="rounded-3xl border border-[var(--line)] bg-white/70 p-5 shadow-sm">
72+
<h2 className="text-sm font-semibold uppercase tracking-[0.16em] text-[var(--sea-ink-soft)]">
73+
Local Todos
74+
</h2>
75+
{error ? (
76+
<p className="mt-3 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
77+
{error}
78+
</p>
79+
) : null}
80+
<form className="mt-4 flex flex-col gap-3 sm:flex-row" onSubmit={addTodo}>
81+
<label className="sr-only" htmlFor="powersync-todo-description">
82+
Todo description
83+
</label>
84+
<input
85+
id="powersync-todo-description"
86+
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"
87+
onChange={(event) => setDescription(event.target.value)}
88+
placeholder="Write to the local PowerSync database"
89+
value={description}
90+
/>
91+
<button
92+
className="rounded-2xl bg-[var(--sea-ink)] px-4 py-3 text-sm font-semibold text-white"
93+
type="submit"
94+
>
95+
Insert Local Row
96+
</button>
97+
</form>
98+
99+
<ul className="mt-5 space-y-3">
100+
{todos.length === 0 ? (
101+
<li className="rounded-2xl border border-dashed border-[var(--line)] px-4 py-5 text-sm text-[var(--sea-ink-soft)]">
102+
No rows yet. Insert one locally, then wire `uploadData()` to send it upstream.
103+
</li>
104+
) : (
105+
todos.map((todo) => (
106+
<li
107+
className="rounded-2xl border border-[var(--line)] bg-[var(--chip-bg)] px-4 py-4"
108+
key={todo.id}
109+
>
110+
<div className="flex items-start justify-between gap-4">
111+
<div>
112+
<p className="font-medium text-[var(--sea-ink)]">{todo.description}</p>
113+
<p className="mt-1 text-xs text-[var(--sea-ink-soft)]">
114+
{todo.created_at}
115+
</p>
116+
</div>
117+
<span className="rounded-full border border-[var(--line)] px-2 py-1 text-xs font-semibold text-[var(--sea-ink-soft)]">
118+
{todo.completed ? 'done' : 'pending'}
119+
</span>
120+
</div>
121+
</li>
122+
))
123+
)}
124+
</ul>
125+
</section>
126+
</div>
127+
</main>
128+
)
129+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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": "Client-visible development token for local testing; VITE_* env vars are exposed to the browser",
21+
"required": false,
22+
"file": ".env.local"
23+
}
24+
],
25+
"integrations": [
26+
{
27+
"type": "vite-plugin",
28+
"path": "powersync-vite-plugin.ts",
29+
"jsName": "powersyncVite",
30+
"code": "powersyncVite()"
31+
},
32+
{
33+
"type": "provider",
34+
"path": "src/integrations/powersync/provider.tsx",
35+
"jsName": "PowerSyncProvider"
36+
}
37+
],
38+
"routes": [
39+
{
40+
"url": "/demo/powersync",
41+
"name": "PowerSync",
42+
"path": "src/routes/demo/powersync.tsx",
43+
"jsName": "PowerSyncDemo"
44+
}
45+
]
46+
}
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)