Get Started

Learn how to use the SerenDB Data API, a ready-to-use REST API built on top of your SerenDB database

SerenDB Auth Building a note-taking app SerenDB Data API demo note-taking app

The SerenDB Data API offers a ready-to-use REST API for your SerenDB database that's compatible with PostgREST. You can interact with any table, view, or function using standard HTTP verbs (GET, POST, PATCH, DELETE). To simplify querying, use client libraries like postgrest-js, postgrest-py, or postgrest-go:

const { data } = await client.from('posts').select('*');

When using the Data API, it is essential to set up RLS policies so that you can safely expose your databases to clients such as web apps. Make sure that all of your tables have RLS policies, and that you have carefully reviewed each policy.

Enable the Data API

Enable the Data API at the branch level for a single database.

Data API and [IP Allow](/docs/manage/projects#configure-ip-allow) cannot be used together. To enable Data API, you must first disable IP Allow on your project.

To get started, open the Data API page from the project sidebar and click Enable.

Data API page with enable button

Once enabled, you'll get:

  • A REST API endpoint for your branch

  • SerenDB Auth as your auth provider

  • Two Postgres roles: authenticated and anonymous (coming soon)

  • GRANT permissions applied to the authenticated role

You can customize the auth provider and GRANTs later, or choose your own auth provider during setup.

Data API enabled view with REST API Endpoint

Secure your Data API

The Data API requires two layers of security:

  1. Database permissions (GRANT statements, already configured if you accepted the defaults)

  2. Row-Level Security (RLS) policies

Database permissions

If you accepted the defaults during setup, SerenDB automatically applied the necessary GRANT statements. If you skipped that step, you'll need to run these SQL statements manually:

-- For existing tables
GRANT SELECT, UPDATE, INSERT, DELETE ON ALL TABLES
  IN SCHEMA public TO authenticated;

-- For future tables
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT, UPDATE, INSERT, DELETE ON TABLES TO authenticated;

-- For sequences (for identity columns)
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO authenticated;

-- Schema usage
GRANT USAGE ON SCHEMA public TO authenticated;

**All requests to the Data API currently require authentication** with a valid JWT token. Anonymous access is not supported yet, but is coming soon. In the near future, we'll provide public/long-lived tokens for anonymous users.

Create a table with RLS

Here's a sample posts table secured with RLS. The GRANT statements above give authenticated users access to all tables, which allows the Data API to work. RLS policies then control which specific rows each user can see and modify.

For guidance on writing RLS policies, see our PostgreSQL RLS tutorial for the basics, or our recommended Drizzle RLS guide for a simpler approach.

<CodeTabs labels={["SQL", "Drizzle (crudPolicy)", "Drizzle (pgPolicy)"]}>

CREATE TABLE "posts" (
	"id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
	"userId" text DEFAULT (auth.user_id()) NOT NULL,
	"content" text NOT NULL,
	"published" boolean DEFAULT false NOT NULL
);

-- Enable RLS and create policies
ALTER TABLE "posts" ENABLE ROW LEVEL SECURITY;

-- When RLS is enabled, all operations are denied by default unless explicitly allowed by policies.

CREATE POLICY "Allow authenticated users to read any post" ON "posts"
AS PERMISSIVE FOR SELECT TO "authenticated"
USING (true);

CREATE POLICY "Allow authenticated users to insert their own posts" ON "posts"
AS PERMISSIVE FOR INSERT TO "authenticated"
WITH CHECK ((select auth.user_id() = "userId"));

CREATE POLICY "Allow authenticated users to update their own posts" ON "posts"
AS PERMISSIVE FOR UPDATE TO "authenticated"
USING ((select auth.user_id() = "userId"))
WITH CHECK ((select auth.user_id() = "userId"));

CREATE POLICY "Allow authenticated users to delete their own posts" ON "posts"
AS PERMISSIVE FOR DELETE TO "authenticated"
USING ((select auth.user_id() = "userId"));
import { sql } from 'drizzle-orm';
import { crudPolicy, authenticatedRole, authUid } from 'drizzle-orm/neon';
import { bigint, boolean, pgTable, text } from 'drizzle-orm/pg-core';

export const posts = pgTable(
  'posts',
  {
    id: bigint({ mode: 'number' }).primaryKey(),
    userId: text()
      .notNull()
      .default(sql`(auth.user_id())`),
    content: text().notNull(),
    published: boolean().notNull().default(false),
  },
  (table) => [
    // Policy for authenticated users
    crudPolicy({
      role: authenticatedRole,
      read: true, // Can also read all posts
      modify: authUid(table.userId), // Can only modify their own posts
    }),
  ]
);
import { sql } from 'drizzle-orm';
import { authenticatedRole, authUid } from 'drizzle-orm/neon';
import { bigint, boolean, pgPolicy, pgTable, text } from 'drizzle-orm/pg-core';

export const posts = pgTable(
  'posts',
  {
    id: bigint({ mode: 'number' }).primaryKey(),
    userId: text()
      .notNull()
      .default(sql`(auth.user_id())`),
    content: text().notNull(),
    published: boolean().notNull().default(false),
  },
  (table) => [
    // Authenticated users
    pgPolicy('Allow authenticated users to read any post', {
      to: authenticatedRole,
      for: 'select',
      using: sql`true`,
    }),
    pgPolicy('Allow authenticated users to insert their own posts', {
      to: authenticatedRole,
      for: 'insert',
      withCheck: authUid(table.userId),
    }),
    pgPolicy('Allow authenticated users to update their own posts', {
      to: authenticatedRole,
      for: 'update',
      using: authUid(table.userId),
      withCheck: authUid(table.userId),
    }),
    pgPolicy('Allow authenticated users to delete their own posts', {
      to: authenticatedRole,
      for: 'delete',
      using: authUid(table.userId),
    }),
  ]
);

The auth.user_id() function is provided by the Data API and extracts the user ID from JWT tokens, making it available to your RLS policies for enforcing per-user access control.

With the posts table and its RLS policies in place, you can now securely query and modify posts using the Data API.

Query from your app

The SerenDB Auth SDK (Stack Auth) manages JWT tokens automatically. Here's an example showing how to use it with postgrest-js:

import { PostgrestClient } from '@supabase/postgrest-js';
import { useUser } from '@stackframe/stack';

// Example: fetch notes for the current user
async function fetchUserNotes() {
  const user = useUser(); // [!code highlight]
  if (!user) return null;

  const { accessToken } = await user.getAuthJson(); // [!code highlight]
  const pg = new PostgrestClient(import.meta.env.VITE_DATA_API_URL, {
    headers: { Authorization: `Bearer ${accessToken}` }, // [!code highlight]
  });

  const { data, error } = await pg
    .from('notes')
    .select('id, title, created_at, owner_id, shared')
    .eq('owner_id', user.id) // [!code highlight]
    .order('created_at', { ascending: false }); // [!code highlight]

  return { data, error };
}

This example shows the key steps:

  1. Get the current user with useUser()

  2. Extract their JWT token with user.getAuthJson()

  3. Create a PostgrestClient with proper authentication headers

  4. Query the Data API with filtering (.eq('owner_id', user.id)) and ordering (.order('created_at', { ascending: false }))

To see a complete, working example of an application built with the Data API, SerenDB Auth, and Postgres RLS, check out our demo note-taking app:

Refresh schema cache

When you modify your database schema (adding tables, columns, or changing structure), the Data API needs to refresh its cache.

  • Console: go to Data API section and click Refresh schema cache

  • SQL: run NOTIFY pgrst, 'reload schema';

Last updated