React and Next.js
Build a React or Next.js app on Plexivo Postgres: sign-in, a live list that updates by itself, and a server route that runs SQL with the secret key.
This guide builds a small notes app with the Next.js App Router: people sign up, see only their own notes, and watch them change live on every device. A server route adds a report with raw SQL. The components work in any React app, such as one made with Vite.
Before you begin#
- A Plexivo Postgres with the Data API on, and
http://localhost:3000in its allowed origins. See Build a backend in five minutes. - The
notestable below, created with the secret key.
create table notes (
id bigint generated always as identity primary key,
owner uuid not null default auth.uid(),
body text not null,
created_at timestamptz not null default now()
);
alter table notes enable row level security;
create policy notes_owner on notes for all to authenticated
using (owner = auth.uid()) with check (owner = auth.uid());
create trigger plexivo_realtime after insert or update or delete on notes
for each row execute function auth.realtime_notify('id');1. Install and configure#
npx create-next-app@latest notes
cd notes
npm install @plexivo/clientIn .env.local, set NEXT_PUBLIC_PLEXIVO_DATA_URL and NEXT_PUBLIC_PLEXIVO_PUBLISHABLE_KEY for the browser, and PLEXIVO_DATA_URL and PLEXIVO_SECRET_KEY for the server. Only names that start with NEXT_PUBLIC_ reach the browser, so the secret key never does.
Create one client for the whole app:
import { createClient } from '@plexivo/client';
export const db = createClient(
process.env.NEXT_PUBLIC_PLEXIVO_DATA_URL,
process.env.NEXT_PUBLIC_PLEXIVO_PUBLISHABLE_KEY
);2. Know who is signed in#
A hook that follows the session. The client keeps it in localStorage and refreshes its token, so a returning visitor is still signed in:
'use client';
import { useEffect, useState } from 'react';
import { db } from './plexivo';
export function useSession() {
const [session, setSession] = useState(() => db.auth.getSession());
useEffect(() => {
const { data: { subscription } } = db.auth.onAuthStateChange((_event, next) => setSession(next));
return () => subscription.unsubscribe();
}, []);
return session;
}3. Sign in#
'use client';
import { useState } from 'react';
import { db } from '../lib/plexivo';
export function SignIn() {
const [error, setError] = useState(null);
async function submit(event) {
event.preventDefault();
const form = new FormData(event.currentTarget);
const credentials = { email: form.get('email'), password: form.get('password') };
try {
if (event.nativeEvent.submitter?.name === 'create') await db.auth.signUp(credentials);
else await db.auth.signInWithPassword(credentials);
} catch (e) {
setError(e.code === 'invalid_credentials' ? 'Wrong email or password.' : e.message);
}
}
return (
<form onSubmit={submit}>
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Password" minLength={10} required />
<button name="signin">Sign in</button>
<button name="create">Create account</button>
{error && <p role="alert">{error}</p>}
</form>
);
}4. A live list#
Load the notes, then apply each change as it arrives. Row-level security means the list only ever holds the signed-in user’s notes:
'use client';
import { useEffect, useState } from 'react';
import { db } from '../../lib/plexivo';
import { useSession } from '../../lib/use-session';
import { SignIn } from '../sign-in';
export default function NotesPage() {
const session = useSession();
const [notes, setNotes] = useState([]);
useEffect(() => {
if (!session) return;
db.from('notes').order('created_at', 'desc').limit(50).then(setNotes);
const feed = db.channel('changes:notes')
.onChange(({ type, record, old }) => setNotes((list) => {
if (type === 'insert') return [record, ...list.filter((n) => n.id !== record.id)];
if (type === 'update') return list.map((n) => (n.id === record.id ? record : n));
return list.filter((n) => n.id !== old.id);
}))
.subscribe();
return () => feed.unsubscribe();
}, [session?.user?.id]);
async function add(event) {
event.preventDefault();
const input = event.currentTarget.elements.body;
await db.from('notes').create({ body: input.value });
input.value = '';
}
if (!session) return <SignIn />;
return (
<main>
<form onSubmit={add}>
<input name="body" placeholder="New note" required />
<button>Add</button>
</form>
<ul>{notes.map((n) => <li key={n.id}>{n.body}</li>)}</ul>
<button onClick={() => db.auth.signOut()}>Sign out</button>
</main>
);
}A note added on another device appears here too, because the channel delivers every change the user may read. If the connection drops or the token expires, the channel rejoins by itself.
5. A server route with SQL#
Route handlers run on your server, so they may hold the secret key and run raw SQL. Check who is asking before you answer: here, any signed-in user may read the total.
import { createClient } from '@plexivo/client';
const server = createClient(process.env.PLEXIVO_DATA_URL, process.env.PLEXIVO_SECRET_KEY, { persistSession: false });
export async function GET(request) {
const token = request.headers.get('authorization')?.replace('Bearer ', '');
const user = token && await createClient(process.env.PLEXIVO_DATA_URL, process.env.NEXT_PUBLIC_PLEXIVO_PUBLISHABLE_KEY, {
persistSession: false, initialSession: { access_token: token }
}).auth.getUser().catch(() => null);
if (!user) return Response.json({ error: 'Sign in first' }, { status: 401 });
const [stats] = await server.sql`select count(*)::int as notes, count(distinct owner)::int as writers from notes`;
return Response.json(stats);
}Call it from a component with the user’s token: fetch('/api/stats', { headers: { authorization: `Bearer ${session.access_token}` } }).
NEXT_PUBLIC_ name or import the server client from a component. If it reaches a browser, the client refuses to start, and the Data API refuses any request that carries it from a web page.Next steps#
Updated 28 October 2026