> ## Documentation Index
> Fetch the complete documentation index at: https://docs.powersync.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Next.js + PowerSync

> Learn how to build highly responsive Next.js applications with PowerSync.

## Introduction

This guide shows you how to add PowerSync to a Next.js app for local-first data and sync. You install the Web SDK, define a client-side schema, and connect sync so components query the local database instead of blocking on the network.

<Tip>
  For a runnable reference that includes API routes for JWTs, JWKS, and writes to Postgres, clone the [Next.js demo app](https://github.com/powersync-ja/powersync-js/tree/main/demos/example-nextjs) in the [powersync-js](https://github.com/powersync-ja/powersync-js) repository and follow its README (Docker, local Postgres, and the PowerSync Service).
</Tip>

<Note>
  PowerSync is tailored for client-side applications — there isn't much benefit to using SSR with PowerSync. Some frameworks like Next.js push towards enabling SSR by default, which means code is evaluated in a Node.js runtime. The PowerSync Web SDK requires browser APIs which are not available in Node.js. For ergonomics, the SDK performs no-ops if used in Node.js (rather than throwing errors), but you should not expect any data from PowerSync during server-side rendering. If you are using SSR in your application, we recommend explicitly isolating PowerSync to client-side code.
</Note>

## Setup

### Next.js Project Setup

Let's start by bootstrapping a new Next.js application using [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

<CodeGroup>
  ```shell npm theme={null}
  npx create-next-app@latest my-powersync-app
  ```

  ```shell yarn theme={null}
  yarn create next-app my-powersync-app
  ```

  ```shell pnpm theme={null}
  pnpm create next-app my-powersync-app
  ```
</CodeGroup>

When running this command you'll be presented with a few options. The PowerSync suggested selection for the setup options Next.js offers are:

```shell theme={null}
Would you like to use TypeScript?  Yes
Would you like to use ESLint?  Yes
Would you like to use Tailwind CSS?  Yes
Would you like your code inside a `src/` directory?  Yes
Would you like to use App Router? (recommended)   Yes
Would you like to use Turbopack for `next dev`?  Yes
Would you like to customize the import alias (`@/*` by default)?  Yes
```

<Info>
  Turbopack is supported in Next.js 16+. If you're using an older version of Next.js, see the [Webpack configuration (legacy)](#webpack-configuration-legacy) section below.
</Info>

### Install PowerSync Dependencies

Using PowerSync in a Next.js application will require the use of the [PowerSync Web SDK](https://www.npmjs.com/package/@powersync/web) and it's peer dependencies.

In addition to this we'll also install [`@powersync/react`](https://www.npmjs.com/package/@powersync/react), which provides several hooks and providers for easier integration.

<CodeGroup>
  ```shell npm theme={null}
  npm install @powersync/web @journeyapps/wa-sqlite @powersync/react
  ```

  ```shell yarn theme={null}
  yarn add @powersync/web @journeyapps/wa-sqlite @powersync/react
  ```

  ```shell pnpm theme={null}
  pnpm install @powersync/web @journeyapps/wa-sqlite @powersync/react
  ```
</CodeGroup>

<Note>This SDK currently requires [@journeyapps/wa-sqlite](https://www.npmjs.com/package/@journeyapps/wa-sqlite) as a peer dependency.</Note>

### Copy Worker Assets

When using Turbopack, you need to copy the PowerSync worker files to your public directory. Add a `postinstall` script to your `package.json`:

```json package.json theme={null}
{
  "scripts": {
    "postinstall": "powersync-web copy-assets -o public"
  }
}
```

Then run the script to copy the assets:

<CodeGroup>
  ```shell npm theme={null}
  npm run postinstall
  ```

  ```shell yarn theme={null}
  yarn postinstall
  ```

  ```shell pnpm theme={null}
  pnpm postinstall
  ```
</CodeGroup>

This copies the pre-bundled worker files to `public/@powersync/`, which are required since Turbopack doesn't support dynamic imports of workers yet.

<Note>Add `public/@powersync/*` to your `.gitignore` file since these are generated assets.</Note>

## Next.js Config Setup

For Next.js 16+ with Turbopack, the configuration is minimal:

```typescript next.config.ts theme={null}
module.exports = {
  images: {
    disableStaticImages: true
  },
  turbopack: {}
};
```

Run `pnpm dev` to start the development server and check that everything compiles correctly, before moving onto the next section.

### Webpack configuration (legacy)

If you're using an older version of Next.js (before 16) or prefer to use Webpack, use this configuration instead:

```typescript next.config.ts theme={null}
module.exports = {
    webpack: (config: any, { isServer }: any) => {
        config.experiments = {
            ...config.experiments,
            asyncWebAssembly: true,
            topLevelAwait: true,
        };

        if (!isServer) {
            config.module.rules.push({
                test: /\.wasm$/,
                type: "asset/resource",
            });
        }

        return config;
    }
}
```

## Configure a PowerSync Instance

Now that we've got our project setup, let's create a new PowerSync Cloud instance and connect our client to it.
For the purposes of this demo, we'll be using Supabase as the backend source database that PowerSync will connect to.

To set up a new PowerSync instance, follow the steps covered in the [Installation - Database Connection](/configuration/source-db/connection) docs page.

## Configure PowerSync in Your Project

### Add core PowerSync files

Start by adding a new directory in `./src/lib` named `powersync`.

#### `AppSchema`

Create a new file called `AppSchema.ts` in the newly created `powersync` directory and add your App Schema to the file. Here is an example of this.

```typescript lib/powersync/AppSchema.ts theme={null}
import { column, Schema, Table } from '@powersync/web';

const lists = new Table({
  created_at: column.text,
  name: column.text,
  owner_id: column.text
});

const todos = new Table(
  {
    list_id: column.text,
    created_at: column.text,
    completed_at: column.text,
    description: column.text,
    created_by: column.text,
    completed_by: column.text,
    completed: column.integer
  },
  { indexes: { list: ['list_id'] } }
);

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

// For types
export type Database = (typeof AppSchema)['types'];
export type TodoRecord = Database['todos'];
// OR:
// export type Todo = RowType<typeof todos>;
export type ListRecord = Database['lists'];
```

This defines the local SQLite database schema and PowerSync will hydrate the tables once the SDK connects to the PowerSync instance.

#### `BackendConnector`

Create a new file called `BackendConnector.ts` in the `powersync` directory and add the following to the file.

```typescript lib/powersync/BackendConnector.ts theme={null}
import { AbstractPowerSyncDatabase, PowerSyncBackendConnector, UpdateType } from '@powersync/web';

export class BackendConnector implements PowerSyncBackendConnector {
    private powersyncUrl: string | undefined;
    private powersyncToken: string | undefined;

    constructor() {
        this.powersyncUrl = process.env.NEXT_PUBLIC_POWERSYNC_URL;
        // This token is for development only.
        // For production applications, integrate with an auth provider or custom auth.
        this.powersyncToken = process.env.NEXT_PUBLIC_POWERSYNC_TOKEN;
    }

    async fetchCredentials() {
        // TODO: Use an authentication service or custom implementation here.
        if (this.powersyncToken == null || this.powersyncUrl == null) {
            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) {
              // The data that needs to be changed in the remote db
              const record = { ...op.opData, id: op.id };
              switch (op.op) {
                case UpdateType.PUT:
                  // TODO: Instruct your backend API to CREATE a record
                  break;
                case UpdateType.PATCH:
                  // TODO: Instruct your backend API to PATCH a record
                  break;
                case UpdateType.DELETE:
                  //TODO: Instruct your backend API to DELETE a record
                  break;
              }
            }
            await transaction.complete();
        } catch (error: any) {
            console.error(`Data upload error - discarding`, error);
            await transaction.complete();
        }
    }
}
```

There are two core functions to this file:

* `fetchCredentials()` - Used to return a JWT token to the PowerSync Service for authentication.
* `uploadData()` - Used to upload changes captured in the local SQLite database that need to be sent to the backend source database, in this case Supabase. We'll get back to this further down.

You'll notice that we need to add a `.env` file to our project which will contain two variables:

* `NEXT_PUBLIC_POWERSYNC_URL` - This is the PowerSync instance url. You can grab this from the PowerSync Cloud dashboard.
* `NEXT_PUBLIC_POWERSYNC_TOKEN` - For development purposes we'll be using a development token. To generate one, please follow the steps outlined in [Development Token](/configuration/auth/development-tokens) from our installation docs.

### Create the PowerSync Provider

Add a new file at `./src/lib/powersync/PowerSyncProvider.tsx`. This is the client-side boundary that initializes the `PowerSyncDatabase` and exposes it via React Context.

```typescript lib/powersync/PowerSyncProvider.tsx theme={null}
'use client';

import { AppSchema } from '@/lib/powersync/AppSchema';
import { BackendConnector } from '@/lib/powersync/BackendConnector';
import { PowerSyncContext } from '@powersync/react';
import { PowerSyncDatabase, WASQLiteOpenFactory, createBaseLogger, LogLevel } from '@powersync/web';
import React, { Suspense } from 'react';

const logger = createBaseLogger();
logger.useDefaults();
logger.setLevel(LogLevel.DEBUG);

let dbInstance: PowerSyncDatabase | null = null;

function getDB(): PowerSyncDatabase {
  if (dbInstance) return dbInstance;

  dbInstance = new PowerSyncDatabase({
    database: new WASQLiteOpenFactory({
      dbFilename: 'powersync.db',
      // Use the pre-bundled worker from public/@powersync/
      // This is required since Turbopack doesn't support dynamic imports of workers yet
      worker: '/@powersync/worker/WASQLiteDB.umd.js'
    }),
    schema: AppSchema,
    flags: { disableSSRWarning: true },
    // Use the pre-bundled sync worker from public/@powersync/
    sync: { worker: '/@powersync/worker/SharedSyncImplementation.umd.js' },
    logger
  });

  dbInstance.connect(new BackendConnector());
  return dbInstance;
}

export function PowerSyncProvider({ children }: { children: React.ReactNode }) {
  return (
    <Suspense>
      <PowerSyncContext.Provider value={getDB()}>{children}</PowerSyncContext.Provider>
    </Suspense>
  );
}
```

The `'use client'` directive marks this as a Client Component — PowerSync requires browser APIs, so initialization must happen on the client. The lazy `getDB()` singleton ensures the database is only instantiated once (and only in the browser), which avoids duplicate connections when modules are re-evaluated during development.

The worker paths point to the pre-bundled workers copied to the public directory by the `powersync-web copy-assets` command. `db.connect()` is called with an instance of `BackendConnector`, which will validate the token returned from `fetchCredentials` and start syncing with the PowerSync Service.

#### Update `layout.tsx`

Update `RootLayout` to wrap `children` with the `PowerSyncProvider`. Importantly, **the root layout should remain a Server Component** — do not add `'use client'` here. This is the standard Next.js App Router convention and is required if you want to export `metadata` (or use `generateMetadata`) from the layout. The client boundary is drawn at `PowerSyncProvider`, which is all that's needed to get PowerSync running in the browser.

```typescript app/layout.tsx theme={null}
import type { Metadata } from 'next';
import { PowerSyncProvider } from '@/lib/powersync/PowerSyncProvider';
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: 'PowerSync Next.js Example'
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
        <PowerSyncProvider>{children}</PowerSyncProvider>
      </body>
    </html>
  );
}
```

#### Use PowerSync

##### Reading Data

In our `page.tsx` we can now use the `useQuery` hook or other PowerSync functions to read data from the SQLite database and render the results in our application.

```typescript app/page.tsx theme={null}
'use client';

import { useState, useEffect } from 'react';
import { useQuery, useStatus, usePowerSync } from '@powersync/react';

export default function Page() {
    // Hook
    const powersync = usePowerSync();

    // Get database status information e.g. downloading, uploading and lastSycned dates
    const status = useStatus();

    // Example 1: Reactive Query
    const { data: lists } = useQuery("SELECT * FROM lists");

    // Example 2: Standard query
    const [lists, setLists] = useState([]);
    useEffect(() => {
        powersync.getAll('SELECT * from lists').then(setLists)
    }, []);

    return (
    <ul>
      {lists.map(list => <li key={list.id}>{list.name}</li>)}
    </ul>
  )
}
```

##### Writing Data

Using the `execute` function we can also write data into our local SQLite database.

```typescript theme={null}
await powersync.execute("INSERT INTO lists (id, created_at, name, owner_id) VALUES (?, ?, ?, ?)", [uuid(), new Date(), "Test", user_id]);
```

Changes made against the local data will be stored in the upload queue and will be processed by the `uploadData` in the BackendConnector class.
