> Full Neon documentation index: https://neon.com/docs/llms.txt
# Getting started with Neon Auth and Neon Data API using React
Build a Todo app using React, Neon Auth, and the Neon Data API
This guide will walk you through building a secure Todo application using **React**, [Neon Auth](https://neon.com/docs/auth/overview), and the [Neon Data API](https://neon.com/docs/data-api/overview).
By the end of this tutorial, you'll have a fully functional Todo app that allows users to sign up, log in, and manage their todos. Authentication is handled through Neon Auth, while secure data access is powered by the Neon Data API. The app does not require any backend server; all interactions happen directly between the React frontend and the Neon database.
This architecture keeps things simple yet secure, with all the complexities of authentication and data access managed by Neon.
- **Identity managed in the database:** User accounts and sessions are stored within the `neon_auth` schema.
- **Direct and secure data access:** The React frontend communicates with the database through the Data API, eliminating the need for a backend.
- **Row-Level Security (RLS) in action:** Policies ensure that each user can only view and modify their own todos.
## Prerequisites
Before you begin, ensure you have the following:
- **Node.js:** Version `18` or later installed on your machine. You can download it from [nodejs.org](https://nodejs.org/).
- **Neon account:** A free Neon account. If you don't have one, sign up at [Neon](https://console.neon.tech/signup).
## Create a Neon project with Neon Auth and Data API
You'll need to create a Neon project and enable both Neon Auth and the Data API.
1. **Create a Neon project:** Navigate to [Neon Console](https://console.neon.tech) to create a new Neon project. Give your project a name, such as `react-neon-todo`.
2. **Enable Neon Data API with Neon Auth:**
- In your project's dashboard, go to the **Data API** page from the sidebar.
- Ensure **Use Neon Auth** is selected.
- Ensure **Grant public schema access** is enabled.
- Finally, click on the **Enable Data API** button to activate the Data API with Neon Auth.

3. **Copy your credentials:**
- **Data API URL:** Found on the Data API page (e.g., `https://ep-xxx.neon.tech/neondb/rest/v1`).

- **Auth URL:** Found on the **Auth** page (e.g., `https://ep-xxx.neon.tech/neondb/auth`).

- **Database Connection String:** Found on the **Dashboard** (select "Pooled connection").
> The database connection string is used exclusively for Drizzle ORM migrations and should not be exposed in the frontend application.

## Set up the React project
Create a new React project using Vite and install the required dependencies.
### Initialize the app
```bash
npm create vite@latest react-neon-todo -- --template react-ts
cd react-neon-todo && npm install
```
When prompted:
- Select "No" for "Use rolldown-vite (Experimental)?"
- Select "No" for "Install with npm and start now?"
You should see output similar to:
```bash
$ npm create vite@latest react-neon-todo -- --template react-ts
> npx
> "create-vite" react-neon-todo --template react-ts
│
◇ Use rolldown-vite (Experimental)?:
│ No
│
◇ Install with npm and start now?
│ No
│
◇ Scaffolding project in /home/user/react-neon-todo...
│
└ Done.
```
### Install dependencies
You will need the following packages for this project:
- **Neon SDK:** [`@neondatabase/neon-js`](https://www.npmjs.com/package/@neondatabase/neon-js) for interacting with Neon Auth and the Data API.
- **React Router:** [`react-router`](https://www.npmjs.com/package/react-router) for routing between pages.
- **Drizzle ORM:** [`drizzle-orm`](https://www.npmjs.com/package/drizzle-orm) and [`drizzle-kit`](https://www.npmjs.com/package/drizzle-kit) for database schema management and migrations.
```bash
npm install @neondatabase/neon-js react-router drizzle-orm
npm install -D drizzle-kit dotenv
```
### Setup Tailwind CSS
Install Tailwind CSS and the Vite plugin:
```bash
npm install tailwindcss @tailwindcss/vite
```
Add the `@tailwindcss/vite plugin` to your Vite configuration (`vite.config.ts`):
```javascript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
});
```
### Configure environment variables
Create a `.env` file in the root of your project and add the credentials you copied in [Step 1](https://neon.com/guides/react-neon-auth-data-api#create-a-neon-project-with-neon-auth-and-data-api).
```env
# Database connection for Drizzle Migrations
DATABASE_URL="postgresql://user:pass@ep-id.pooler.region.neon.tech/neondb?sslmode=require&channel_binding=require"
# Public variables for the React App
VITE_NEON_DATA_API_URL="https://ep-xxx.us-east-1.aws.neon.tech/neondb/rest/v1"
VITE_NEON_AUTH_URL="https://ep-xxx.aws.neon.tech/neondb/auth"
```
## Set up Drizzle ORM
**Important: Why Drizzle ORM?** This guide uses Drizzle ORM to define **Row-Level Security (RLS)** policies declaratively in TypeScript, but it is not required. You can use any Postgres-compatible tool or raw SQL. If you prefer SQL, you can reference the scripts in the [GitHub repository](https://github.com/dhanushreddy291/react-neon-todo/blob/main/drizzle/0001_salty_hedge_knight.sql) which are the equivalent of the Drizzle schema and migrations shown here.
Drizzle is used only for **managing the database** (migrations). The React application itself uses the **Neon JS SDK** to query data via the Data API.
Drizzle ORM helps manage your database schema and migrations. It will be used to define the schema for the `todos` table and to interact with the Neon Auth tables. In addition, you will configure [Row‑Level Security (RLS)](https://neon.com/postgresql/postgresql-administration/postgresql-row-level-security) policies to ensure that users can only access their own data.
### Create Drizzle config
Create a `drizzle.config.ts` file in the project root:
```typescript
import 'dotenv/config';
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
schemaFilter: ['public', 'neon_auth'],
dbCredentials: {
url: process.env.DATABASE_URL!,
},
} satisfies Config;
```
This config tells Drizzle Kit where to find your database schema and where to output migration files. The `schemaFilter` is configured to look at both the `public` and `neon_auth` schemas. The `neon_auth` schema is where Neon Auth stores its user data.
### Pull Neon Auth schema
A key feature of Neon Auth is the automatic creation and maintenance of the Better Auth tables within the `neon_auth` schema. Since these tables reside in your Neon database, you can work with them directly using SQL queries or any Postgres‑compatible ORM, including defining foreign key relationships.
To integrate Neon Auth tables into your Drizzle ORM setup, you need to introspect the existing `neon_auth` schema and generate the corresponding Drizzle schema definitions.
This step is crucial because it makes Drizzle aware of the Neon Auth tables, allowing you to create relationships between your application data (like the `todos` table) and the user data managed by Neon Auth.
1. **Introspect the database:**
Run the Drizzle Kit `pull` command to generate a schema file based on your existing Neon database tables.
```bash
npx drizzle-kit pull
```
This command connects to your Neon database, inspects its structure, and creates `schema.ts` and `relations.ts` files inside a new `drizzle` folder. This file will contain the Drizzle schema definition for the Neon Auth tables.
2. **Organize schema files:**
Create a new directory `src/db`. Move the generated `schema.ts` and `relations.ts` files from the `drizzle` directory to `src/db/schema.ts` and `src/db/relations.ts` respectively.
```
├ 📂 drizzle
│ ├ 📂 meta
│ ├ 📜 migration.sql
│ ├ 📜 relations.ts ────────┐
│ └ 📜 schema.ts ───────────┤
├ 📂 src │
│ ├ 📂 db │
│ │ ├ 📜 relations.ts <─────┤
│ │ └ 📜 schema.ts <────────┘
│ └ 📜 App.tsx
└ …
```
3. **Add the Todos table to your schema**
Open `src/db/schema.ts` to view the `neon_auth` tables that Drizzle generated from your existing Neon database schema. At the bottom of the file, append the `todos` table definition along with the RLS policies shown below.
You will also need to import the following additional utilities at the top of the file, as they are not included by default:
- `bigint` from `drizzle-orm/pg-core` to define the `id` column of the `todos` table.
- `authenticatedRole` and `crudPolicy` from `drizzle-orm/neon` to configure Row-Level Security (RLS).
Drizzle ORM includes built-in support for RLS policies. The `authenticatedRole` represents the role assigned to authenticated users, while `crudPolicy` provides a declarative way to define RLS policies. For more details, see the [Simplify RLS with Drizzle](https://neon.com/docs/guides/rls-drizzle) guide.
```typescript {9,12,40-60}
import {
pgTable,
pgSchema,
uuid,
text,
timestamp,
unique,
boolean,
bigint,
} from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { authenticatedRole, crudPolicy } from 'drizzle-orm/neon';
export const neonAuth = pgSchema('neon_auth');
// .. other Neon Auth table definitions ..
export const userInNeonAuth = neonAuth.table(
'user',
{
id: uuid().defaultRandom().primaryKey().notNull(),
name: text().notNull(),
email: text().notNull(),
emailVerified: boolean().notNull(),
image: text(),
createdAt: timestamp({ withTimezone: true, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp({ withTimezone: true, mode: 'string' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
role: text(),
banned: boolean(),
banReason: text(),
banExpires: timestamp({ withTimezone: true, mode: 'string' }),
},
(table) => [unique('user_email_key').on(table.email)]
);
export const todos = pgTable(
'todos',
{
id: bigint('id', { mode: 'number' }).primaryKey().generatedByDefaultAsIdentity().notNull(),
text: text('text').notNull(),
completed: boolean('completed').notNull().default(false),
user_id: uuid('user_id')
.notNull()
.references(() => userInNeonAuth.id),
},
(table) => [
crudPolicy({
role: authenticatedRole,
// Type casting user_id to text for comparison with auth.user_id()
read: sql`(select auth.user_id() = ${table.user_id}::text)`,
modify: sql`(select auth.user_id() = ${table.user_id}::text)`,
}),
]
);
export type Todo = typeof todos.$inferSelect;
```
The `todos` table contains the following columns: `id`, `text`, `completed`, and `user_id`. It is linked to the `userInNeonAuth` (`user`) table in the `neon_auth` schema and uses the `crudPolicy` function to define RLS policies.
1. **Foreign key reference**
The `todos` table includes a foreign key to the `user` table in the `neon_auth` schema.
2. **RLS policy (`crudPolicy`)**
This policy ensures that each user can only read and modify their own todos.
3. **Authenticated User ID**
The `auth.user_id()` function retrieves the ID of the currently authenticated user.
4. **Access control enforcement**
The `user_id` column in the `todos` table is compared against the value returned by `auth.user_id()` to enforce access control.
5. **Type casting**
The `user_id` is cast to `text` to ensure compatibility between the UUID type in the table and the text type returned by `auth.user_id()`.
### Generate and apply migrations
Now, generate the SQL migration file to create the `todos` table.
```bash
npx drizzle-kit generate
```
This creates a new SQL file in the `drizzle` directory. Apply this migration to your Neon database by running:
**Important: Issue with commented migrations** This is a [known issue](https://github.com/drizzle-team/drizzle-orm/issues/4851) in Drizzle. If `drizzle-kit pull` generated an initial migration file (e.g., `0000_...sql`) wrapped in block comments (`/* ... */`), `drizzle-kit migrate` may fail with an `unterminated /* comment` error.
To resolve this, manually delete the contents of the `0000_...sql` file or replace the block comments with line comments (`--`).
```bash
npx drizzle-kit migrate
```
Your `todos` table now exists in your Neon database. You can verify this in the **Tables** section of your Neon project dashboard.
Now that the database schema is set up, you can proceed to build the React application.
## Configure Neon Auth and Data API
### Initialize the Neon client
Create a file `src/neon.ts`. This initializes the Neon client, which handles both Authentication and Data API queries. For React hooks support, you will use the `BetterAuthReactAdapter`.
```typescript
import { createClient } from '@neondatabase/neon-js';
import { BetterAuthReactAdapter } from '@neondatabase/neon-js/auth/react/adapters';
export const neon = createClient({
auth: {
url: import.meta.env.VITE_NEON_AUTH_URL,
adapter: BetterAuthReactAdapter(),
},
dataApi: {
url: import.meta.env.VITE_NEON_DATA_API_URL,
},
});
```
### Application entry point
Update `src/main.tsx` to wrap your app in the `NeonAuthUIProvider` and `BrowserRouter` to enable routing and authentication context.
```tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import { NeonAuthUIProvider } from '@neondatabase/neon-js/auth/react/ui';
import App from './App.tsx';
import { neon } from './neon.ts';
import './index.css';
createRoot(document.getElementById('root')!).render(
No tasks yet.
} {todos.map((todo) => (