Quickstart: Next.js
Build the notes app with the Next.js App Router: sign up and in from a client component, list and add notes as the signed-in user, and read them in a route handler with the user's own token.
A Next.js 15 app (App Router, TypeScript) where people sign up, sign in, add notes and see only their own. It takes about ten minutes.
#Before you start
Do steps 1 to 3 of the Quickstart first: create a project, copy its project URL and anon key (Quickstart), and create the notes table with its row rules (Quickstart). Every signed-in user then sees and changes only their own notes, whatever the app code does.
#1. Create the app and install the SDK
npx create-next-app@latest notes --ts --app --eslint --no-tailwind --src-dir --import-alias "@/*"
cd notes
npm install @sovaria/sdk#2. Add your project URL and anon key
NEXT_PUBLIC_SOVARIA_URL=https://<project-ref>.sovarias.in
NEXT_PUBLIC_SOVARIA_ANON_KEY=<anon-key>Both values are public: anything prefixed NEXT_PUBLIC_ ends up in the browser bundle, and the anon key only does what your rules allow visitors to do. Never put the service role key in a NEXT_PUBLIC_ variable.
#3. Create the client
import { createClient } from '@sovaria/sdk'
// One client for the browser. The signed-in session is kept in localStorage and refreshed on its own.
export const sovaria = createClient(
process.env.NEXT_PUBLIC_SOVARIA_URL!,
process.env.NEXT_PUBLIC_SOVARIA_ANON_KEY!,
)
// data.sql answers { columns, rows, row_count }; rows are arrays in column order.
export function toObjects<T>(result: { columns: string[]; rows: unknown[][] }): T[] {
return result.rows.map((row) => Object.fromEntries(result.columns.map((c, i) => [c, row[i]])) as T)
}#4. Sign in and work with notes
'use client'
import { useEffect, useState } from 'react'
import { SovariaError } from '@sovaria/sdk'
import { sovaria, toObjects } from '@/lib/sovaria'
type Note = { id: number; body: string; created_at: string }
export default function Home() {
const [session, setSession] = useState(() => sovaria.auth.getSession())
const [notes, setNotes] = useState<Note[]>([])
const [error, setError] = useState('')
// INITIAL_SESSION, SIGNED_IN, TOKEN_REFRESHED, SIGNED_OUT. Returns a function that stops listening.
useEffect(() => sovaria.auth.onAuthStateChange((_event, s) => setSession(s)), [])
async function load() {
const result = await sovaria.data.sql('select id, body, created_at from notes order by id desc')
setNotes(toObjects<Note>(result as never))
}
useEffect(() => { if (session) load().catch(show) }, [session])
function show(err: unknown) {
setError(err instanceof SovariaError ? err.message : String(err))
}
async function auth(form: FormData, mode: 'in' | 'up') {
setError('')
const email = String(form.get('email')), password = String(form.get('password'))
try {
if (mode === 'up') await sovaria.auth.signUp(email, password)
else await sovaria.auth.signIn(email, password)
} catch (err) { show(err) }
}
async function add(form: FormData) {
try {
await sovaria.data.sql('insert into notes (body) values ($1)', [String(form.get('body'))])
await load()
} catch (err) { show(err) }
}
if (!session) {
return (
<form action={(f) => auth(f, 'in')}>
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Password" required />
<button>Sign in</button>
<button formAction={(f) => auth(f, 'up')}>Create account</button>
{error && <p role="alert">{error}</p>}
</form>
)
}
return (
<main>
<form action={add}>
<input name="body" placeholder="Write a note" required maxLength={2000} />
<button>Add</button>
</form>
{error && <p role="alert">{error}</p>}
<ul>{notes.map((n) => <li key={n.id}>{n.body}</li>)}</ul>
<button onClick={() => sovaria.auth.signOut()}>Sign out</button>
</main>
)
}signUpcreates the user and signs them in. If the project asks people to confirm their email first, it answersemail_verification_required: trueand no session until they click the link (Email and password).- Values go in the second argument (
$1,$2, …) and the database binds them, so a note can never change the SQL. - The query has no
where owner_id = …: the row rules from the Quickstart add it for every request.
#5. Read notes on the server as the user
A route handler has no browser session. Send the user's access token in the Authorization header and create a client per request with it, so the same rules apply on the server.
import { createClient } from '@sovaria/sdk'
import { toObjects } from '@/lib/sovaria'
export async function GET(req: Request) {
const token = req.headers.get('authorization')?.replace(/^Bearer /, '')
if (!token) return new Response('Sign in first', { status: 401 })
const sovaria = createClient(process.env.NEXT_PUBLIC_SOVARIA_URL!, process.env.NEXT_PUBLIC_SOVARIA_ANON_KEY!, {
persistSession: false,
}).setAccessToken(token)
const result = await sovaria.data.sql('select id, body from notes order by id desc limit 50')
return Response.json(toObjects(result as never))
}#6. Run it
npm run dev # http://localhost:3000New projects allow http://localhost:*, so local development works at once. Before you deploy, add your site's origin under Project settings › API (Allowed origins (CORS) for browser apps), or the browser gets 403 ORIGIN_NOT_ALLOWED.
#Next steps
- Check who can read and change
notesbefore you ship: Tutorial: prove your table is private before you ship. - Push new notes to other devices as they happen: Database change events.
- Every SDK method: JavaScript SDK. Server patterns: JavaScript: React, Next.js, Vite and Node.
- Deploy on Vercel with the integration: Vercel and Netlify.
#Coming from Firebase or Supabase?
| Platform | What this is called there |
|---|---|
| Firebase | Firebase with Next.js |
| Supabase | Supabase Next.js quickstart |
| Sovaria | Quickstart: Next.js |
Moving an existing app? Migrate from Supabase or Firebase imports users (with their passwords), data, files and rules.