Skip to content

feat: SSR compatibility with Next.js example #128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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
48 changes: 48 additions & 0 deletions examples/nextjs/todo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Todo Example App - Next.js

A todo application built with Next.js 15 (App Router), TanStack DB, and TypeScript.

## Features

- **Next.js 15** with App Router
- **TanStack DB** with both Electric and Query collections
- **Real-time updates** with Electric SQL
- **Optimistic mutations** for instant UI feedback
- **TypeScript** for type safety
- **Tailwind CSS** for styling
- **PostgreSQL** database with Drizzle ORM

## How to run

- Go to the root of the repository and run:

- `pnpm install`
- `pnpm build`

- Install packages
`pnpm install`

- Start dev server & Docker containers
`pnpm dev`

- Run db migrations
`pnpm db:push`

## Architecture

This example demonstrates the same functionality as the React version but using Next.js App Router:

- **App Router**: Uses the latest Next.js app directory structure
- **Client Components**: The main todo interface is a client component for interactivity
- **Server Components**: Layout and other components use server components where possible
- **API Routes**: Express server runs separately for database operations
- **Real-time sync**: Electric SQL provides real-time database synchronization

## Collection Types

The app supports two collection types:

1. **Query Collections**: Traditional API-based data fetching with polling
2. **Electric Collections**: Real-time streaming updates via Electric SQL

You can switch between collection types using the toggle in the UI to see the difference in behavior.
38 changes: 38 additions & 0 deletions examples/nextjs/todo/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
version: "3.8"
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_DB: todo_app
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "54322:5432"
volumes:
- ./postgres.conf:/etc/postgresql/postgresql.conf:ro
tmpfs:
- /var/lib/postgresql/data
- /tmp
command:
- postgres
- -c
- config_file=/etc/postgresql/postgresql.conf
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5

electric:
image: electricsql/electric:canary
environment:
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/todo_app?sslmode=disable
ELECTRIC_INSECURE: true
ports:
- 3003:3000
depends_on:
postgres:
condition: service_healthy

volumes:
postgres_data:
15 changes: 15 additions & 0 deletions examples/nextjs/todo/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Config } from "drizzle-kit"

export default {
schema: `./src/db/schema.ts`,
out: `./drizzle`,
dialect: `postgresql`,
casing: `snake_case`,
dbCredentials: {
host: process.env.DB_HOST || `localhost`,
port: parseInt(process.env.DB_PORT || `54322`),
user: process.env.DB_USER || `postgres`,
password: process.env.DB_PASSWORD || `postgres`,
database: process.env.DB_NAME || `todo_app`,
},
} satisfies Config
7 changes: 7 additions & 0 deletions examples/nextjs/todo/drizzle/0000_whole_sprite.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE "todos" (
"id" serial PRIMARY KEY NOT NULL,
"text" text NOT NULL,
"completed" boolean DEFAULT false NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
8 changes: 8 additions & 0 deletions examples/nextjs/todo/drizzle/0001_sturdy_titania.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE "config" (
"id" serial PRIMARY KEY NOT NULL,
"key" text NOT NULL,
"value" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "config_key_unique" UNIQUE("key")
);
29 changes: 29 additions & 0 deletions examples/nextjs/todo/drizzle/0002_update_timestamps_trigger.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- Custom SQL migration file, put your code below! --

-- Create a function to update the updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Create trigger for todos table
DROP TRIGGER IF EXISTS update_todos_updated_at ON "todos";
CREATE TRIGGER update_todos_updated_at
BEFORE UPDATE ON "todos"
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

-- Create trigger for config table
DROP TRIGGER IF EXISTS update_config_updated_at ON "config";
CREATE TRIGGER update_config_updated_at
BEFORE UPDATE ON "config"
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

-- Insert default config for background color
INSERT INTO "config" ("key", "value")
VALUES ('backgroundColor', '#f5f5f5')
ON CONFLICT ("key") DO NOTHING;
65 changes: 65 additions & 0 deletions examples/nextjs/todo/drizzle/meta/0000_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"id": "1649b703-2b2d-413c-8185-15390a9d97e7",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.todos": {
"name": "todos",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"text": {
"name": "text",
"type": "text",
"primaryKey": false,
"notNull": true
},
"completed": {
"name": "completed",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
116 changes: 116 additions & 0 deletions examples/nextjs/todo/drizzle/meta/0001_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{
"id": "1b3a815a-1865-451b-b646-983e995ee97e",
"prevId": "1649b703-2b2d-413c-8185-15390a9d97e7",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.config": {
"name": "config",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"config_key_unique": {
"name": "config_key_unique",
"nullsNotDistinct": false,
"columns": ["key"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.todos": {
"name": "todos",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"text": {
"name": "text",
"type": "text",
"primaryKey": false,
"notNull": true
},
"completed": {
"name": "completed",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
Loading