Skip to main content
Realtime apps store data locally and sync to the server in the background. Reads are instant from local SQLite. Writes queue up and sync automatically. The app works the same whether the user is online, offline, or on a flaky connection. Built on PowerSync + Neon Postgres.

How it works

User writes → Local SQLite (instant) → Upload queue → Server → Postgres

User reads  ← Local SQLite (instant) ← PowerSync replication ←───┘
The browser keeps a local SQLite database. All reads hit local storage — no network round-trip, no loading spinners. Writes go into an upload queue that syncs to Postgres through a process component. Changes from other users replicate back down through PowerSync.

Getting started

Ask the AI to build a realtime app. It handles the full setup — database, sync layer, template, wiring.
AI tools used behind the scenes:
  • create_database { name: "my-db" } — provisions Neon Postgres
  • database_sync_enable { databaseId: "..." } — deploys PowerSync on Fly.io
  • create_app { name: "my-app", template: "realtime" } — scaffolds the app
  • connect_app_to_database { appId: "...", databaseId: "..." } — wires credentials
The realtime template includes a PowerSync client, sync handler, and migration runner out of the box.

Template structure

UI (src/)
  • src/lib/powersync.ts — PowerSync client and connector
  • src/lib/schema.ts — table/column schema (mirrors Postgres)
  • src/main.tsx — wraps the app in PowerSyncContext.Provider
Process (server/)
  • GET /api/powersync/token — signs Ed25519 JWT for auth
  • POST /api/sync — handles CRUD uploads to Postgres
  • Migration runner and sync rule updater

Key patterns

Write locally first. Never wait for a server response before updating what the user sees. Write to local SQLite and let the upload queue handle sync.
await db.writeTransaction(async (tx) => {
  await tx.execute(
    'INSERT INTO todos (id, title, user_id) VALUES (?, ?, ?)',
    [id, title, userId]
  );
});
Empty states, not spinners. After first sync, local SQLite has the data. Show “No items yet” instead of a loading indicator. Atomic writes. Use writeTransaction to group related changes. Subscribers see the final state, not partial updates. Conflict resolution. Last-write-wins by default. The sync handler in POST /api/sync controls how uploads merge with server state — customize it for more complex strategies. Offline resilience. PowerSync queues writes when offline and syncs them on reconnect. The app doesn’t need to know or care whether it’s online.

Schema updates

  1. Write a migration SQL file
  2. Commit — migrations run automatically
  3. Update src/lib/schema.ts to match the new columns
  4. PowerSync picks up the change and syncs

Auth flow

PowerSync client calls fetchCredentials() → the process component signs an Ed25519 JWT → PowerSync verifies it against inline JWKS. Include the user ID in the JWT for per-user sync rules.

Environment variables

Set automatically when you connect a database to the app:
VariableComponentPurpose
VITE_POWERSYNC_URLUIPowerSync instance URL
VITE_POWERSYNC_TOKEN_ENDPOINTUIToken endpoint
VITE_DATABASE_URLUIPooled Postgres URI
DIRECT_DATABASE_URLProcessDirect Postgres URI
POWERSYNC_SIGNING_KEYProcessEd25519 private key
POWERSYNC_API_TOKENProcessAdmin API auth
POWERSYNC_URLProcessAdmin API URL

When to use this

Good fitOverkill
Task managers, note appsStatic marketing sites
Collaborative toolsOne-time form submissions
Field apps with spotty connectivityRead-only dashboards
Anything where instant feel mattersSimple CRUD with no offline need