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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# PowerSync

This project includes the PowerSync Web SDK and React hooks.

## Environment

Set these variables in `.env.local`:

- `VITE_POWERSYNC_URL`
- `VITE_POWERSYNC_TOKEN` for local development only

## What The Add-on Includes

- `src/lib/powersync/AppSchema.ts`
- `src/lib/powersync/BackendConnector.ts`
- `src/integrations/powersync/provider.tsx`
- `src/routes/demo/powersync.tsx`

## Next Steps

1. Replace the development token flow in `src/lib/powersync/BackendConnector.ts` with your real auth flow.
2. Update the sample schema in `src/lib/powersync/AppSchema.ts` to match your synced tables.
3. Implement the upload logic in `uploadData()` so local mutations are written back to your backend.

PowerSync setup guidance:
https://docs.powersync.com/client-sdk-references/js-web
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

VITE_POWERSYNC_URL=
VITE_POWERSYNC_TOKEN=
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Plugin } from 'vite'

export default function powersyncVite(): Plugin {
return {
name: 'powersync-vite',
config() {
return {
optimizeDeps: {
exclude: ['@powersync/web'],
},
worker: {
format: 'es',
},
}
},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ReactNode } from 'react'
import { PowerSyncContext } from '@powersync/react'
import { PowerSyncDatabase, WASQLiteOpenFactory } from '@powersync/web'

import { AppSchema } from '#/lib/powersync/AppSchema'
import { BackendConnector } from '#/lib/powersync/BackendConnector'

const db = new PowerSyncDatabase({
database: new WASQLiteOpenFactory({
dbFilename: 'powersync.db',
}),
schema: AppSchema,
flags: {
disableSSRWarning: true,
},
})

void db.connect(new BackendConnector())

export default function PowerSyncProvider({
children,
}: {
children: ReactNode
}) {
return <PowerSyncContext.Provider value={db}>{children}</PowerSyncContext.Provider>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Schema, Table, column } from '@powersync/web'

const todos = new Table(
{
created_at: column.text,
description: column.text,
completed: column.integer,
},
{ indexes: { created_at: ['created_at'] } },
)

export const AppSchema = new Schema({
todos,
})

export type Database = (typeof AppSchema)['types']
export type TodoRecord = Database['todos']
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
type AbstractPowerSyncDatabase,
type PowerSyncBackendConnector,
UpdateType,
} from '@powersync/web'

export class BackendConnector implements PowerSyncBackendConnector {
private readonly powersyncUrl = import.meta.env.VITE_POWERSYNC_URL
private readonly powersyncToken = import.meta.env.VITE_POWERSYNC_TOKEN

async fetchCredentials() {
if (!this.powersyncUrl || !this.powersyncToken) {
return null
}

return {
endpoint: this.powersyncUrl,
token: this.powersyncToken,
}
}

async uploadData(database: AbstractPowerSyncDatabase): Promise<void> {
const transaction = await database.getNextCrudTransaction()

if (!transaction) {
return
}

try {
for (const op of transaction.crud) {
const record = { ...op.opData, id: op.id }

switch (op.op) {
case UpdateType.PUT:
console.info('TODO: create record remotely', record)
break
case UpdateType.PATCH:
console.info('TODO: patch record remotely', record)
break
case UpdateType.DELETE:
console.info('TODO: delete record remotely', record)
break
}
}

await transaction.complete()
} catch (error) {
console.error('PowerSync uploadData failed', error)
throw error
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { useState } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { usePowerSync, useQuery, useStatus } from '@powersync/react'

export const Route = createFileRoute('/demo/powersync')({
component: PowerSyncDemo,
})

type TodoRow = {
id: string
created_at: string
description: string
completed: number
}

function PowerSyncDemo() {
const powerSync = usePowerSync()
const status = useStatus()
const { data } = useQuery(
'SELECT id, created_at, description, completed FROM todos ORDER BY created_at DESC',
)
const todos = (data ?? []) as Array<TodoRow>
const [description, setDescription] = useState('')
const [error, setError] = useState<string | null>(null)

async function addTodo(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()

const nextDescription = description.trim()
if (!nextDescription) {
return
}

try {
setError(null)
await powerSync.execute(
'INSERT INTO todos (id, created_at, description, completed) VALUES (?, ?, ?, ?)',
[crypto.randomUUID(), new Date().toISOString(), nextDescription, 0],
)

setDescription('')
} catch (error) {
console.error('Failed to insert PowerSync todo', error)
setError('Failed to insert row. Please try again.')
}
}

return (
<main className="page-wrap py-10">
<div className="max-w-3xl space-y-6">
<header className="space-y-2">
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-[var(--sea-ink-soft)]">
Offline Sync
</p>
<h1 className="text-3xl font-semibold tracking-tight">PowerSync</h1>
<p className="text-sm text-[var(--sea-ink-soft)]">
This demo writes to the local SQLite database immediately. Replace the sample
schema and backend connector with your real PowerSync configuration.
</p>
</header>

<section className="rounded-3xl border border-[var(--line)] bg-white/70 p-5 shadow-sm">
<h2 className="text-sm font-semibold uppercase tracking-[0.16em] text-[var(--sea-ink-soft)]">
Connection State
</h2>
<pre className="mt-3 overflow-auto rounded-2xl bg-[var(--chip-bg)] p-4 text-xs leading-6 text-[var(--sea-ink)]">
{JSON.stringify(status, null, 2)}
</pre>
</section>

<section className="rounded-3xl border border-[var(--line)] bg-white/70 p-5 shadow-sm">
<h2 className="text-sm font-semibold uppercase tracking-[0.16em] text-[var(--sea-ink-soft)]">
Local Todos
</h2>
{error ? (
<p className="mt-3 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{error}
</p>
) : null}
<form className="mt-4 flex flex-col gap-3 sm:flex-row" onSubmit={addTodo}>
<label className="sr-only" htmlFor="powersync-todo-description">
Todo description
</label>
<input
id="powersync-todo-description"
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"
onChange={(event) => setDescription(event.target.value)}
placeholder="Write to the local PowerSync database"
value={description}
/>
<button
className="rounded-2xl bg-[var(--sea-ink)] px-4 py-3 text-sm font-semibold text-white"
type="submit"
>
Insert Local Row
</button>
</form>

<ul className="mt-5 space-y-3">
{todos.length === 0 ? (
<li className="rounded-2xl border border-dashed border-[var(--line)] px-4 py-5 text-sm text-[var(--sea-ink-soft)]">
No rows yet. Insert one locally, then wire `uploadData()` to send it upstream.
</li>
) : (
todos.map((todo) => (
<li
className="rounded-2xl border border-[var(--line)] bg-[var(--chip-bg)] px-4 py-4"
key={todo.id}
>
<div className="flex items-start justify-between gap-4">
<div>
<p className="font-medium text-[var(--sea-ink)]">{todo.description}</p>
<p className="mt-1 text-xs text-[var(--sea-ink-soft)]">
{todo.created_at}
</p>
</div>
<span className="rounded-full border border-[var(--line)] px-2 py-1 text-xs font-semibold text-[var(--sea-ink-soft)]">
{todo.completed ? 'done' : 'pending'}
</span>
</div>
</li>
))
)}
</ul>
</section>
</div>
</main>
)
}
46 changes: 46 additions & 0 deletions packages/create/src/frameworks/react/add-ons/powersync/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "PowerSync",
"description": "Add PowerSync offline sync to your application.",
"phase": "add-on",
"type": "add-on",
"category": "database",
"color": "#2563EB",
"priority": 55,
"link": "https://docs.powersync.com/client-sdk-references/js-web",
"modes": ["file-router"],
"envVars": [
{
"name": "VITE_POWERSYNC_URL",
"description": "PowerSync instance URL",
"required": true,
"file": ".env.local"
},
{
"name": "VITE_POWERSYNC_TOKEN",
"description": "Client-visible development token for local testing; VITE_* env vars are exposed to the browser",
"required": false,
"file": ".env.local"
}
],
"integrations": [
{
"type": "vite-plugin",
"path": "powersync-vite-plugin.ts",
"jsName": "powersyncVite",
"code": "powersyncVite()"
},
{
"type": "provider",
"path": "src/integrations/powersync/provider.tsx",
"jsName": "PowerSyncProvider"
}
],
"routes": [
{
"url": "/demo/powersync",
"name": "PowerSync",
"path": "src/routes/demo/powersync.tsx",
"jsName": "PowerSyncDemo"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"dependencies": {
"@journeyapps/wa-sqlite": "^1.2.6",
"@powersync/react": "^1.7.4",
"@powersync/web": "^1.26.1"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading