PotaLab
← All examples
SnippetData APIAuthRLSNext.js

Todo app with RLS

One table, four policies. The client never filters by user: Postgres does it. A Server Component guards the page, a client component uses useQuery and useMutation.

localhost:3000/todos

My todos

Signed in as ann@acme.com

2 open

RLS: this query has no user filter. Postgres returns only rows where user_id = auth.uid().

Preview · sample data
'use client'
import { useEffect, useState } from 'react'
import { createBrowserClient } from '@potalab/base/next'
import { useMutation, useQuery } from '@potalab/base/react'
const base = () =>
createBrowserClient({ url: process.env.NEXT_PUBLIC_POTALAB_BASE_URL!, key: process.env.NEXT_PUBLIC_POTALAB_BASE_KEY! })
// createBrowserClient only runs in the browser, so render after mount
export function Todos() {
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
return mounted ? <TodoList /> : <p>Loading...</p>
}
function TodoList() {
const client = base()
const [title, setTitle] = useState('')
// No user filter: RLS only returns this user's rows
const todos = useQuery(
client.from('todos').select('id,title,done').order('created_at', { ascending: false }),
)
// Writes refetch the query above automatically
const add = useMutation((t: string) => client.from('todos').insert({ title: t }))
const toggle = useMutation((v: { id: number; done: boolean }) =>
client.from('todos').update({ done: v.done }).eq('id', v.id),
)
const remove = useMutation((id: number) => client.from('todos').delete().eq('id', id))
if (todos.isPending) return <p>Loading...</p>
if (todos.isError) return <p role="alert">{todos.error.message}</p>
return (
<div>
<form
onSubmit={(e) => {
e.preventDefault()
add.mutate(title)
setTitle('')
}}
>
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="What needs doing?" />
<button type="submit" disabled={add.isPending || !title.trim()}>Add</button>
</form>
<ul>
{todos.data.map((t) => (
<li key={t.id}>
<label>
<input
type="checkbox"
checked={t.done}
onChange={() => toggle.mutate({ id: t.id, done: !t.done })}
/>
<span className={t.done ? 'line-through' : ''}>{t.title}</span>
</label>
<button onClick={() => remove.mutate(t.id)}>Delete</button>
</li>
))}
</ul>
<p>{todos.data.filter((t) => !t.done).length} open</p>
</div>
)
}

Snippet for illustration, written for the Next.js App Router with @potalab/base/next,@potalab/base/react and @potalab/base. Set NEXT_PUBLIC_POTALAB_BASE_URL andNEXT_PUBLIC_POTALAB_BASE_KEY, adapt names and redirect URLs, and review policies before production.

Next: Magic-link sign-in →