Docs

Quickstart: Node.js and Express

Build a notes API with Express that signs users in through Sovaria and runs every query as the calling user, so your row rules apply on the server too.

Updated Oct 24, 2026

An Express API with POST /signup, POST /signin, GET /notes and POST /notes. It never holds a key that can bypass your rules: each request runs as the user whose token it carries.

#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 project

mkdir notes-api && cd notes-api
npm init -y && npm pkg set type=module
npm install express @sovaria/sdk
SOVARIA_URL=https://<project-ref>.sovarias.in
SOVARIA_ANON_KEY=<anon-key>

#2. The server

import express from 'express'
import { createClient, SovariaError } from '@sovaria/sdk'

const { SOVARIA_URL, SOVARIA_ANON_KEY } = process.env
const app = express()
app.use(express.json())

// A fresh client per request. persistSession: false keeps nothing between requests.
const anonymous = () => createClient(SOVARIA_URL, SOVARIA_ANON_KEY, { persistSession: false })
const asUser = (req) => {
  const token = req.get('authorization')?.replace(/^Bearer /, '')
  if (!token) throw Object.assign(new Error('Sign in first'), { statusCode: 401 })
  return anonymous().setAccessToken(token)
}
// data.sql answers { columns, rows, row_count }; rows are arrays in column order.
export const toObjects = ({ columns, rows }) =>
  rows.map((row) => Object.fromEntries(columns.map((c, i) => [c, row[i]])))

// Answers { access_token, refresh_token, expires_in, … }; the app keeps the tokens.
app.post('/signup', async (req, res, next) => {
  try { res.json(await anonymous().auth.signUp(req.body.email, req.body.password)) } catch (e) { next(e) }
})
app.post('/signin', async (req, res, next) => {
  try { res.json(await anonymous().auth.signIn(req.body.email, req.body.password)) } catch (e) { next(e) }
})

app.get('/notes', async (req, res, next) => {
  try { res.json(toObjects(await asUser(req).data.sql('select id, body, created_at from notes order by id desc'))) }
  catch (e) { next(e) }
})
app.post('/notes', async (req, res, next) => {
  try {
    const r = await asUser(req).data.sql('insert into notes (body) values ($1) returning id, body', [req.body.body])
    res.status(201).json(toObjects(r)[0])
  } catch (e) { next(e) }
})

app.use((err, _req, res, _next) => {
  const status = err.statusCode || 500
  res.status(status).json({ error: err instanceof SovariaError ? err.code : 'ERROR', message: err.message })
})

app.listen(3000, () => console.log('http://localhost:3000'))

#3. Run and try it

node --env-file=.env server.js

# in another terminal
TOKEN=$(curl -s localhost:3000/signup -H 'Content-Type: application/json' \
  -d '{"email":"ada@example.com","password":"Correct-Horse-9!"}' | jq -r .access_token)
curl -s localhost:3000/notes -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"body":"From Express"}'
curl -s localhost:3000/notes -H "Authorization: Bearer $TOKEN"
  • Server-to-server calls send no browser Origin, so allowed origins don't apply here.
  • Need to act as your app rather than a user (a nightly job, a webhook)? Use the service role key from a secret store, on the server only. It bypasses every rule, so keep those code paths small (API keys).
  • To check a token without calling Sovaria, verify it against your project's JWKS (Sessions and tokens).

#Next steps

#Coming from Firebase or Supabase?

PlatformWhat this is called there
FirebaseFirebase Admin SDK with Express
SupabaseSupabase with Express
SovariaQuickstart: Node.js and Express

Moving an existing app? Migrate from Supabase or Firebase imports users (with their passwords), data, files and rules.