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
20 changes: 16 additions & 4 deletions demos/example-nextjs/.env.local.template
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
# Copy to .env.local, and complete these variables.
# Leave blank to test local-only.
NEXT_PUBLIC_POWERSYNC_URL=
NEXT_PUBLIC_POWERSYNC_TOKEN=
# Copy this file: cp .env.local.template .env.local
#
# App-level environment variables for Next.js.
# Docker Compose variables live in powersync/docker/.env.

# ── PowerSync service ────────────────────────────────────────────────────────
POWERSYNC_URL=http://localhost:8080

# Used by Next.js API routes (localhost, since it runs outside Docker)
DATABASE_URL=postgresql://postgres:changeme@localhost:5432/postgres

# ── JWT key pair (required) ──────────────────────────────────────────────────
# Generate with: pnpm generate-keys
# Then paste the output below.
POWERSYNC_PRIVATE_KEY=
POWERSYNC_PUBLIC_KEY=
6 changes: 6 additions & 0 deletions demos/example-nextjs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@ yarn-debug.log*
# local env files
.env*.local

# unignore docker env (overrides root .gitignore)
!powersync/docker/.env

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# override root gitignore's "lib" rule (which targets build output)
!src/library/

# ide
.idea
.fleet
Expand Down
123 changes: 116 additions & 7 deletions demos/example-nextjs/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,119 @@
# PowerSync Next.js example
# PowerSync Next.js Example

This example is built using [Next.js](https://nextjs.org/) and the [PowerSync JS web SDK](https://docs.powersync.com/client-sdk-references/js-web).
PowerSync demo using [Next.js](https://nextjs.org/) and the [PowerSync JS web SDK](https://docs.powersync.com/client-sdk-references/js-web).

To see it in action:
Syncs data from a local Postgres through a self-hosted PowerSync service. No login required; the Next.js server hands out anonymous JWTs signed with a local key pair.

1. `cd` into this directory and run `pnpm install`.
2. Copy `.env.local.template` to `.env.local`, and complete the environment variables. You can generate a [temporary development token](https://docs.powersync.com/usage/installation/authentication-setup/development-tokens), or leave blank to test with local-only data.
3. Run `pnpm start`.
4. Open the localhost URL displayed in the terminal output in your browser.
## Architecture

```
Browser (WASQLite)
↕ sync (WebSocket)
PowerSync service <-> Postgres (source DB)
↕ bucket storage
Postgres (storage DB)

Browser -> POST /api/data -> Next.js API route -> Postgres (writes)
Browser -> GET /api/auth/token -> Next.js API route -> signed JWT
PowerSync -> GET /api/auth/keys -> Next.js API route -> JWKS (public key)
```

## Prerequisites

- [Docker](https://www.docker.com/) (running)
- [PowerSync CLI](https://docs.powersync.com/self-hosting/installation) (`npm i -g @powersync/cli`)
- Node.js >= 18 and [pnpm](https://pnpm.io/)

## Setup

```bash
# 1. Install deps
pnpm install

# 2. Create env file
cp .env.local.template .env.local

# 3. Generate a JWT key pair and paste the output into .env.local
pnpm generate-keys

# 4. Start local Postgres + PowerSync
pnpm local:up

# 5. Start Next.js
pnpm dev
```

Open [http://localhost:3000](http://localhost:3000).

### About the key pair

PowerSync validates JWTs the Next.js app issues by fetching a JWKS at [/api/auth/keys](src/app/api/auth/keys/route.ts). The private key signs tokens; the public key is what PowerSync fetches. Both must be set in `.env.local` — the app will refuse to start token issuance without them.

`pnpm generate-keys` prints base64-encoded JWKs to stdout. Copy the two lines into `.env.local`:

```
POWERSYNC_PRIVATE_KEY=<base64>
POWERSYNC_PUBLIC_KEY=<base64>
```

Restart `pnpm dev` after changing these values.

## Project structure

```
src/
├── app/
│ ├── api/
│ │ ├── auth/
│ │ │ ├── keys/route.ts JWKS endpoint for PowerSync
│ │ │ └── token/route.ts Anonymous JWT endpoint
│ │ └── data/route.ts CRUD writes to Postgres
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── components/
│ ├── CustomerList.tsx
│ ├── StatusPanel.tsx
│ └── SyncedContent.tsx
└── library/
├── auth-keys.ts Loads RSA key pair from env (server only)
├── db.ts Postgres pool (server only)
└── powersync/
├── connector.ts Fetch token + upload mutations
├── powersync-provider.tsx PowerSync context provider
└── schema.ts Client-side table schema
```

## Environment variables

All config lives in `.env.local`. Docker Compose also reads from this file (via a symlink at `powersync/docker/.env`).

| Variable | Required | What it does |
|---|---|---|
| `POWERSYNC_URL` | yes | PowerSync service URL, also used as the JWT audience |
| `DATABASE_URL` | yes | Postgres connection for Next.js API routes (uses `localhost`) |
| `POWERSYNC_PRIVATE_KEY` | yes | Base64-encoded JWK private key — generate with `pnpm generate-keys` |
| `POWERSYNC_PUBLIC_KEY` | yes | Base64-encoded JWK public key — generate with `pnpm generate-keys` |
| `PS_DATABASE_*` | yes | Postgres credentials used by Docker |
| `PS_STORAGE_*` | yes | Separate Postgres for PowerSync internal storage |
| `PS_DATA_SOURCE_URI` | yes | Postgres URI inside Docker (uses `pg-db` hostname) |
| `PS_STORAGE_SOURCE_URI` | yes | Storage Postgres URI inside Docker (uses `pg-storage` hostname) |

## Scripts

| Command | What it does |
|---|---|
| `pnpm dev` | Start Next.js dev server |
| `pnpm build` | Production build |
| `pnpm generate-keys` | Print a fresh JWT key pair for `.env.local` |
| `pnpm local:up` | Start PowerSync + Postgres via Docker |
| `pnpm local:down` | Stop Docker stack |

## Resetting the database

If you change the schema, wipe the Docker volumes so the init SQL runs again:

```bash
powersync docker stop --directory powersync --remove --remove-volumes
powersync docker reset --directory powersync
```
6 changes: 0 additions & 6 deletions demos/example-nextjs/next.config.js

This file was deleted.

10 changes: 10 additions & 0 deletions demos/example-nextjs/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { NextConfig } from 'next';

const config: NextConfig = {
serverExternalPackages: ['pg', 'jose'],
turbopack: {
root: import.meta.dirname
}
};

export default config;
37 changes: 15 additions & 22 deletions demos/example-nextjs/package.json
Original file line number Diff line number Diff line change
@@ -1,46 +1,39 @@
{
"name": "example-nextjs",
"version": "0.1.15",
"version": "0.2.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"clean": "rm -rf .next",
"watch": "next dev",
"start": "next start",
"lint": "next lint",
"test:build": "pnpm build",
"clean": "rm -rf .next",
"generate-keys": "node --experimental-strip-types scripts/generate-keys.mts",
"copy-assets": "powersync-web copy-assets -o public",
"postinstall": "pnpm copy-assets"
"postinstall": "pnpm copy-assets",
"local:up": "powersync docker start --directory powersync",
"local:down": "powersync docker stop --directory powersync",
"test:build": "pnpm build"
},
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@fontsource/roboto": "^5.0.13",
"@journeyapps/wa-sqlite": "^1.6.0",
"@lexical/react": "^0.15.0",
"@mui/icons-material": "^5.15.18",
"@mui/material": "^5.15.18",
"@powersync/react": "^1.10.0",
"@powersync/web": "^1.37.2",
"lato-font": "^3.0.0",
"lexical": "^0.15.0",
"@tailwindcss/postcss": "^4.2.2",
"jose": "^5.9.6",
"next": "^16.1.1",
"pg": "^8.13.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"tailwindcss": "^4.2.2"
},
"devDependencies": {
"@types/node": "^20.12.12",
"@types/pg": "^8.11.10",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"autoprefixer": "^10.4.19",
"babel-loader": "^9.1.3",
"css-loader": "^6.11.0",
"eslint": "^9.0.0",
"eslint-config-next": "^16.0.0",
"postcss": "^8.4.35",
"sass": "^1.77.2",
"sass-loader": "^13.3.3",
"style-loader": "^3.3.4",
"tailwindcss": "^3.4.3"
"typescript": "^5.0.0"
}
}
6 changes: 0 additions & 6 deletions demos/example-nextjs/postcss.config.js

This file was deleted.

7 changes: 7 additions & 0 deletions demos/example-nextjs/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
plugins: {
'@tailwindcss/postcss': {}
}
};

export default config;
11 changes: 11 additions & 0 deletions demos/example-nextjs/powersync/cli.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Adds YAML Schema support for VSCode users with the YAML extension installed. This enables features like validation and autocompletion based on the provided schema.
# yaml-language-server: $schema=https://unpkg.com/@powersync/cli-schemas@latest/json-schema/cli-config.json

# This file is managed by the PowerSync CLI.
# Run \`powersync link --help\` for more information.
type: self-hosted
api_url: http://localhost:8080
api_key: dev-token-do-not-use-in-production
plugins:
docker:
project_name: powersync_example-nextjs
15 changes: 15 additions & 0 deletions demos/example-nextjs/powersync/docker/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# ── Application database (Postgres) ──────────────────────────────────────────
PS_DATABASE_USER=postgres
PS_DATABASE_PASSWORD=changeme
PS_DATABASE_NAME=postgres
PS_DATABASE_PORT=5432

# ── PowerSync storage database (internal) ────────────────────────────────────
PS_STORAGE_USER=postgres
PS_STORAGE_PASSWORD=changeme
PS_STORAGE_DATABASE=powersync_storage
PS_STORAGE_PORT=5433

# ── Docker-internal URIs (pg-db / pg-storage are Docker service names) ────────
PS_DATA_SOURCE_URI=postgresql://postgres:changeme@pg-db:5432/postgres
PS_STORAGE_SOURCE_URI=postgresql://postgres:changeme@pg-storage:5433/powersync_storage
48 changes: 48 additions & 0 deletions demos/example-nextjs/powersync/docker/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Composed PowerSync Docker stack (generated by powersync docker configure).
# Modules add entries to include and to services.powersync.depends_on.
# Relative paths: . = powersync/docker, .. = powersync.
# Include syntax requires Docker Compose v2.20.3+

include:
[
modules/database-postgres/postgres.database.compose.yaml,
modules/storage-postgres/postgres.storage.compose.yaml
]

services:
powersync:
restart: unless-stopped
image: journeyapps/powersync-service:latest
command: [ 'start', '-r', 'unified' ]
env_file:
- .env
volumes:
- ../service.yaml:/config/service.yaml
- ../sync-config.yaml:/config/sync-config.yaml
environment:
POWERSYNC_CONFIG_PATH: /config/service.yaml
NODE_OPTIONS: --max-old-space-size=1000
healthcheck:
test:
- 'CMD'
- 'node'
- '-e'
- "fetch('http://localhost:${PS_PORT:-8080}/probes/liveness').then(r =>
r.ok ? process.exit(0) : process.exit(1)).catch(() =>
process.exit(1))"
interval: 5s
timeout: 1s
retries: 15
ports:
- '${PS_PORT:-8080}:${PS_PORT:-8080}'
# host.docker.internal resolves automatically on macOS/Windows; on Linux
# it needs this mapping so the container can reach the Next.js app running
# on the host (see client_auth.jwks_uri in ../service.yaml).
extra_hosts:
- 'host.docker.internal:host-gateway'
depends_on:
{
pg-db: { condition: service_healthy },
pg-storage: { condition: service_healthy }
}
name: powersync_example-nextjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS customers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT NOT NULL
);

-- Seed data
INSERT INTO customers (id, name, created_at) VALUES
(gen_random_uuid()::text, 'Alice Johnson', now()::text),
(gen_random_uuid()::text, 'Bob Smith', now()::text),
(gen_random_uuid()::text, 'Carol White', now()::text);

-- PowerSync requires a logical replication publication
CREATE PUBLICATION powersync FOR TABLE customers;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Database module: PostgreSQL for replication source (logical replication).
# Paths are relative to this file's directory when using include (Compose resolves from the included file).
# Include from main: path: modules/database-postgres/postgres.database.compose.yaml

services:
pg-db:
image: postgres:18
restart: always
environment:
- POSTGRES_USER=${PS_DATABASE_USER}
- POSTGRES_DB=${PS_DATABASE_NAME}
- POSTGRES_PASSWORD=${PS_DATABASE_PASSWORD}
- PGPORT=${PS_DATABASE_PORT}
volumes:
- pg_data:/var/lib/postgresql
- ./init-scripts:/docker-entrypoint-initdb.d
ports:
- '${PS_DATABASE_PORT}:${PS_DATABASE_PORT}'
command: ['postgres', '-c', 'wal_level=logical']
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${PS_DATABASE_USER} -d ${PS_DATABASE_NAME}']
interval: 5s
timeout: 5s
retries: 5

volumes:
pg_data:
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Storage database is created by the Postgres image from POSTGRES_DB (e.g. powersync_storage).
-- Add any extra schema or grants here if needed.
Loading
Loading