“Database migrations are no longer on our list of worries” - See how Modem.dev uses branching
/Data API/Access control & security

Access control & security

Understand how the Data API authenticates requests and enforces database permissions.

Beta

The Neon Data API is in Beta. Share your feedback on Discord or via the Neon Console.

The Neon Data API is designed to be secure by default. It relies on PostgreSQL's native security model, meaning the API does not have its own separate permission system — it simply acts as a gateway that respects the roles and Row-Level Security (RLS) policies defined in your database.

Securing your data involves two layers:

  1. Role Privileges (GRANT): Determining which tables the API is allowed to access.
  2. Row-Level Security (RLS): Determining which specific rows a user is allowed to see.

API Roles

When the Data API receives an HTTP request, it switches to a specific PostgreSQL role before executing the query. The role chosen depends on whether the request includes an Authorization header.

1. The authenticated role

Used for: Requests with a valid JWT token.

When a client sends a valid Bearer token, the API switches to the authenticated role. This is the primary role for your application users.

  • The JWT token identifies who the user is (via the sub claim).
  • The authenticated role defines what the application is allowed to touch.

2. The anonymous role

Used for: Requests without a token. If a request arrives with no Authorization header, the API switches to the anonymous role.

  • By default, this role has no permissions.
  • You can explicitly GRANT SELECT permissions to this role if you want to expose public data (e.g., a list of products or public blog posts) without requiring users to log in.
  • The GRANT statements would be similar to the grants for the authenticated role. See Configure schema access for an example.

3. Custom roles

The API determines the role based on the role claim in the JWT. If you issue your own tokens with a custom role claim (e.g., "role": "admin"), the API will attempt to switch to a Postgres role named admin. You must ensure this role exists in your database and has the correct permissions.

The following Layers explain how to configure these roles for secure access.

Layer 1: Table Privileges

Before RLS can even apply, the database role must have permission to perform the action (SELECT, INSERT, etc.) on the table.

Automatic configuration

When you enable the Data API via the Console, Neon automatically applies default GRANT statements to the authenticated role for the public schema. See the manual configuration section below for the statements that are applied.

Manual configuration

If you skipped the default Data API setup in the Neon Console or you are adding custom roles or working with schemas other than public, you may need to grant permissions explicitly.

The following example SQL commands grant the authenticated role access to all existing and future tables in the public schema. If your tables are in a different schema (e.g., sales, analytics etc), update the schema name accordingly. You can also substitute authenticated with a custom role (e.g., admin), but you must ensure that the role exists in your database.

Run these commands in the SQL Editor to ensure your API users can access your tables:

-- 1. Grant usage on the schema
GRANT USAGE ON SCHEMA public TO authenticated;

-- 2. Grant access to existing tables
GRANT SELECT, UPDATE, INSERT, DELETE ON ALL TABLES
  IN SCHEMA public TO authenticated;

-- 3. Ensure future tables are automatically accessible
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT, UPDATE, INSERT, DELETE ON TABLES TO authenticated;

-- 4. Grant access to sequences (required for auto-incrementing IDs)
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO authenticated;

Permission denied errors?

If you encounter a "permission denied" error immediately after creating a new table, it is likely because the authenticated role hasn't been granted privileges on it. Running the GRANT commands above usually resolves this.

Layer 2: Row-Level Security (RLS)

Granting SELECT access to the authenticated role allows the API to read all rows in the table. To restrict access to specific data (e.g., "users can only see their own posts"), you must enable Row-Level Security and create policies.

Understanding RLS states

RLS has three distinct states that affect data visibility:

StateBehavior
RLS disabledAll authenticated users see all rows (no filtering)
RLS enabled, no policiesAll access is blocked (users see nothing)
RLS enabled + policiesRows filtered by policy rules (typically using auth.user_id())

RLS disabled means no filtering

If RLS is disabled on a table, any authenticated user can see all rows in that table. This is different from "filtering without policies" — it means there is no filtering at all.

Checking RLS status in the Console

The Data API page in the Neon Console shows the RLS status of your tables. If a table has RLS disabled, you'll see a warning message like:

"table_name has RLS disabled. All authenticated users can view all rows in this table(s)."

You can click the Enable RLS button to enable Row-Level Security on the table without writing SQL. After enabling RLS, you'll need to create policies to allow access.

The user_id column pattern

To filter rows by user, your tables need a column that stores the user's ID. The Data API provides auth.user_id(), a SQL function that extracts the User ID (sub claim) from the current request's JWT. Use this function to:

  1. Set a default value so new rows are automatically associated with the current user
  2. Filter rows in policies so users only see their own data
-- Example table with user_id column
CREATE TABLE posts (
  id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  user_id text DEFAULT (auth.user_id()) NOT NULL,  -- Automatically set to current user
  content text NOT NULL
);

Example workflow

  1. Enable RLS on the table:

    ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

    Once enabled, all access is blocked by default until a policy is created.

  2. Create a policy:

    CREATE POLICY "User owns data" ON posts
      FOR ALL
      TO authenticated
      USING ( select auth.user_id() = user_id )
      WITH CHECK ( select auth.user_id() = user_id );

Now, even though the authenticated role has SELECT permission on the table, the database will only return rows where the user_id column matches the ID in the user's token.

Last updated on

Was this page helpful?