Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .env.cloud.template
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# Supabase Configuration
VITE_SUPABASE_URL=https://<your-project-id>.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<replace-with-your-anon-key>
VITE_SUPABASE_PUBLISHABLE_KEY=sb_publishable_<replace-with-your-publishable-key>

# PowerSync Configuration
VITE_POWERSYNC_URL=https://<your-project-id>.powersync.journeyapps.com
7 changes: 6 additions & 1 deletion .env.local.template
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
# Copy this template: `cp .env.local.template .env.local`
# Edit .env.local and enter your Supabase and PowerSync project details.

# VITE_SUPABASE_PUBLISHABLE_KEY is the default local publishable key printed by
# `supabase start` (run `supabase status` to see it again). Same on every machine.
VITE_SUPABASE_URL=http://localhost:54321
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
VITE_SUPABASE_PUBLISHABLE_KEY=sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH
VITE_POWERSYNC_URL=http://localhost:8080

# Self-hosted PowerSync Configuration
PS_POSTGRESQL_URI=postgresql://postgres:postgres@supabase_db_powersync:5432/postgres
PS_SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
# Supabase Auth JWKS endpoint (for ES256 signing keys). "kong" is the Supabase
# gateway's hostname on the shared Docker network (see docker/compose.yaml).
PS_SUPABASE_JWKS_URI=http://kong:8000/auth/v1/.well-known/jwks.json
PS_API_TOKEN=super-secret
PS_PORT=8080
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ If you prefer using the Supabase CLI to develop the database locally and push it

3. Click on the "Credentials" tab of the "Edit Instance" dialog:
- Tick the "Use Supabase Auth" checkbox and configure the JWT secret.
- Note: newer Supabase projects sign user tokens with asymmetric JWT signing keys (ES256) instead of the legacy shared JWT secret. If that is your case, point PowerSync at your project's JWKS endpoint (`https://<your-project-id>.supabase.co/auth/v1/.well-known/jwks.json`) rather than pasting a shared secret. See the [PowerSync Supabase auth guide](https://docs.powersync.com/integration-guides/supabase-+-powersync) for details. The local Docker setup in this repo is already wired up for this (see `docker/powersync.yaml`).
- Click "Save" to save all the changes to your PowerSync instance. The instance will now be deployed — this may take a minute or two.

<details>
Expand Down Expand Up @@ -149,15 +150,15 @@ Then set the following environment variables in your `.env.local` file:

```bash
VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
VITE_SUPABASE_PUBLISHABLE_KEY=
VITE_POWERSYNC_URL=
```

**Where do you get these values?**

For Supabase, you can get both settings directly from:
- VITE_SUPABASE_URL - [API Settings & URL](https://supabase.com/dashboard/project/_/settings/api)
- VITE_SUPABASE_ANON_KEY - [API Keys](https://supabase.com/dashboard/project/_/settings/api-keys)
- VITE_SUPABASE_PUBLISHABLE_KEY - copy the **publishable** key (`sb_publishable_...`) from [API Keys](https://supabase.com/dashboard/project/_/settings/api-keys)

For PowerSync, follow these steps:
1. Go to your [PowerSync Dashboard](https://powersync.journeyapps.com/)
Expand Down
7 changes: 7 additions & 0 deletions docker/powersync.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ sync_config:

client_auth:
supabase: true
# HS256 shared-secret validation (legacy Supabase JWT signing).
supabase_jwt_secret: !env PS_SUPABASE_JWT_SECRET
# Asymmetric (ES256) validation for Supabase's new JWT signing keys.
# Points at the Supabase Auth JWKS endpoint; PowerSync selects the key by KID.
jwks_uri: !env PS_SUPABASE_JWKS_URI
# Supabase issues user tokens with the "authenticated" audience.
audience:
- authenticated

telemetry:
prometheus_port: 9090
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@journeyapps/wa-sqlite": "^1.4.1",
"@powersync/react": "^1.8.2",
"@powersync/web": "^1.30.0",
"@journeyapps/wa-sqlite": "^1.5.0",
"@powersync/react": "^1.9.0",
"@powersync/web": "^1.34.0",
"@supabase/supabase-js": "^2.87.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
allowBuilds:
'@journeyapps/wa-sqlite': true
esbuild: true
supabase: true
6 changes: 3 additions & 3 deletions src/powersync/SupabaseConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {

export type SupabaseConfig = {
supabaseUrl: string;
supabaseAnonKey: string;
supabaseKey: string;
powersyncUrl: string;
};

Expand Down Expand Up @@ -49,12 +49,12 @@ export class SupabaseConnector
this.config = {
supabaseUrl: import.meta.env.VITE_SUPABASE_URL,
powersyncUrl: import.meta.env.VITE_POWERSYNC_URL,
supabaseAnonKey: import.meta.env.VITE_SUPABASE_ANON_KEY,
supabaseKey: import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY,
};

this.client = createClient(
this.config.supabaseUrl,
this.config.supabaseAnonKey,
this.config.supabaseKey,
{
auth: {
persistSession: true,
Expand Down
149 changes: 104 additions & 45 deletions src/powersync/System.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import {
createBaseLogger,
LogLevel,
PowerSyncDatabase,
SyncClientImplementation,
WASQLiteOpenFactory,
WASQLiteVFS
WASQLiteVFS,
} from "@powersync/web";
import { AppSchema } from "./AppSchema";
import { connector } from "./SupabaseConnector";
Expand All @@ -14,66 +13,126 @@ logger.useDefaults();
logger.setLevel(LogLevel.DEBUG);

/**
* Default configuration AccessHandlePoolVFS - uses IndexedDB
* ✅ Use this for: Simple setup, most browsers
* ❌ Avoid if: You need Safari support or have stability issues
* Detects mobile devices (phones and tablets).
*
* Prefers the modern `navigator.userAgentData.mobile` hint (Chromium) and
* falls back to user-agent sniffing, which is required because Safari does not
* implement `userAgentData`. Note that iPadOS 13+ reports a desktop Mac
* user-agent, so it is detected via touch-point support instead.
*/
// export const powerSync = new PowerSyncDatabase({
// schema: AppSchema,
// database: {
// dbFilename: 'example.db'
// },
// logger: logger
// });
export function isMobile(): boolean {
if (typeof navigator === "undefined") return false;

const uaData = (
navigator as Navigator & { userAgentData?: { mobile?: boolean } }
).userAgentData;
if (typeof uaData?.mobile === "boolean") return uaData.mobile;

const ua = navigator.userAgent;
// iPadOS 13+ looks like macOS; a Mac reporting touch points is really an iPad.
const isIPadOS =
navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;

return /Android|iPhone|iPad|iPod|Mobi/i.test(ua) || isIPadOS;
}

/**
* Detects Apple's Safari / WebKit.
*
* Every browser on iOS/iPadOS (including Chrome and Firefox) is WebKit under
* the hood and shares Safari's OPFS limitations, so iOS is always treated as
* Safari. On desktop we match Safari but exclude Chromium- and Firefox-based
* browsers, which also carry "Safari" in their user-agent string.
*/
export function isSafari(): boolean {
if (typeof navigator === "undefined") return false;

const ua = navigator.userAgent;
const isIOS =
/iPhone|iPad|iPod/i.test(ua) ||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
if (isIOS) return true;

return (
/Safari/i.test(ua) &&
!/Chrome|Chromium|Edg|OPR|Firefox|FxiOS|CriOS/i.test(ua)
);
}

/**
* Detects OPFS availibility
*
* Checking if the OPFS related functions are availible
*/
export function isOPFSAvailable(): boolean {
return (
typeof navigator !== "undefined" &&
typeof navigator.storage?.getDirectory === "function" &&
typeof Worker === "function"
);
}

/**
* Alternative configuration with OPFS storage (Origin Private File System)
*
* 🚀 RECOMMENDED: Use OPFSCoopSyncVFS for production apps
*
* ✅ When to use:
* - You need multi-tab support across ALL browsers (including Safari)
* - Better performance than IndexedDB
* - Safari/iOS compatibility is important
*
* ❌ When NOT to use:
* - Safari incognito mode (known issues)
* - You prefer simpler setup
*
* Alternative: Change to WASQLiteVFS.AccessHandlePoolVFS for single-tab apps with best performance
*
* 📚 Learn more: https://docs.powersync.com/client-sdk-references/javascript-web#sqlite-virtual-file-systems
* Checks whether OPFS actually works, not just whether the API exists.
*
* Safari Private Browsing exposes the OPFS API but rejects when you request the
* directory, so calling getDirectory() distinguishes a usable OPFS from an
* unusable one. Browsers that expose the API otherwise support it.
*/
export async function isOPFSUsable(): Promise<boolean> {
if (!isOPFSAvailable()) return false;
try {
await navigator.storage.getDirectory();
return true;
} catch {
return false;
}
}

export function pickVFS(opfsUsable: boolean = isOPFSAvailable()): WASQLiteVFS {
const safari = isSafari();
const mobile = isMobile();
const multiTab = typeof SharedWorker !== "undefined";

// Fall back to IndexedDB (IDBBatchAtomicVFS) when OPFS can't be used:
// - OPFS is not usable at all: no API, or Safari Private Browsing where the
// API exists but createSyncAccessHandle fails (see isOPFSUsable), or
// - mobile Safari (iOS/iPadOS), where OPFS is not supported, or
// - desktop Safari with multi-tab, due to aggressive tab suspension from Safari
const forceIndexedDB = !opfsUsable || (safari && (mobile || multiTab));

const vfs = forceIndexedDB
? WASQLiteVFS.IDBBatchAtomicVFS
: WASQLiteVFS.OPFSCoopSyncVFS;

console.log(
`[powersync] using VFS: ${vfs} (safari=${safari}, mobile=${mobile}, multiTab=${multiTab}, opfsUsable=${opfsUsable})`,
);
return vfs;
}

const opfsUsable = await isOPFSUsable();
const enableMultiTabs = typeof SharedWorker !== "undefined";

export const powerSync = new PowerSyncDatabase({
database: new WASQLiteOpenFactory({
dbFilename: "exampleVFS.db",
vfs: WASQLiteVFS.OPFSCoopSyncVFS, // Use AccessHandlePoolVFS for single-tab only
vfs: pickVFS(opfsUsable),
flags: {
enableMultiTabs: typeof SharedWorker !== "undefined",
enableMultiTabs,
},
}),
flags: {
enableMultiTabs: typeof SharedWorker !== "undefined",
enableMultiTabs,
},
schema: AppSchema,
logger: logger,
});

/**
* Quick Decision Guide:
*
* 🎯 Most apps → Use OPFSCoopSyncVFS (uncomment above)
* 📱 Safari users → Must use OPFSCoopSyncVFS
* ⚡ Single tab only → Use AccessHandlePoolVFS
* 🔧 Quick prototype → Keep default (IndexedDB)
*/

// Sign in the user anonymously to Supabase (creates a temporary user session)
await connector.signInAnonymously();

// Establish connection between PowerSync and the Supabase connector
powerSync.connect(connector, {
// Rust based implementation is more efficient and faster than the JavaScript implementation
clientImplementation: SyncClientImplementation.RUST,
crudUploadThrottleMs: 5000
});
powerSync.connect(connector, {
crudUploadThrottleMs: 5000,
});
Loading