Skip to content

feat(adapter): PostgresJS Adapter w WebAuthentication #13091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/3_bug_adapter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ body:
- "@auth/mongodb-adapter"
- "@auth/neo4j-adapter"
- "@auth/pg-adapter"
- "@auth/pgjs-adapter"
- "@auth/pouchdb-adapter"
- "@auth/prisma-adapter"
- "@auth/sequelize-adapter"
Expand Down
1 change: 1 addition & 0 deletions .github/pr-labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mongodb: ["packages/adapter-mongodb/**/*"]
neo4j: ["packages/adapter-neo4j/**/*"]
next-auth: ["packages/next-auth/**/*"]
pg: ["packages/adapter-pg/**/*"]
pgjs: ["packages/adapter-pgjs/**/*"]
neon: ["packages/adapter-neon/**/*"]
playgrounds: ["apps/playgrounds/**/*"]
pouchdb: ["packages/adapter-pouchdb/**/*"]
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ on:
- "@auth/mongodb-adapter"
- "@auth/neo4j-adapter"
- "@auth/pg-adapter"
- "@auth/pgjs-adapter"
- "@auth/pouchdb-adapter"
- "@auth/prisma-adapter"
- "@auth/sequelize-adapter"
Expand Down
1 change: 1 addition & 0 deletions docs/pages/data/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"mongodb": "MongoDB",
"neo4j": "Neo4j",
"pg": "pg",
"pgjs": "PostgresJS",
"pouchdb": "PouchDB",
"sequelize": "Sequelize",
"surrealdb": "SurrealDB",
Expand Down
3 changes: 2 additions & 1 deletion docs/pages/getting-started/adapters/_meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export default {
mongodb: "MongoDB",
neo4j: "Neo4j",
neon: "Neon",
pg: "PostgreSQL",
pg: "PostgreSQL - PG",
pgjs: "PostgreSQL - PostgresJS",
pouchdb: "PouchDB",
prisma: "Prisma",
sequelize: "Sequelize",
Expand Down
320 changes: 320 additions & 0 deletions docs/pages/getting-started/adapters/pgjs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
import { Code } from "@/components/Code"
import { Callout } from "nextra/components"

<img align="right" src="/img/adapters/pgjs.svg" height="64" width="64" />

# PostgresJS Adapter

## Resources

- [Official PostgreSQL Docs](https://www.postgresql.org/docs/)
- [PostgresJS Client Docs](https://github.com/porsager/postgres)

## Setup

### Installation

```bash npm2yarn
npm install @auth/pgjs-adapter postgres
```

### Environment Variables

```ts
import postgres from "postgres"
// Default connection
const sql = postgres(process.env.PGSQL)

// Connection instance + options. Refer to PostgresJS docs for full info
const sql = postgres(process.env.PGSQL, {
host: "", // Postgres ip address[s] or domain name[s]
port: 5432, // Postgres server port[s]
database: "", // Name of database to connect to
username: "", // Username of database user
password: "", // Password
max: 20, // max connections
})
```

### Configuration

<Code>
<Code.Next>

```ts filename="./auth.ts"
import NextAuth from "next-auth"
import PostgresJSAdapter from "@auth/pgjs-adapter"
import postgres from "postgres"

const sql = postgres(process.env.PGSQL, {
host: "", // Postgres ip address[s] or domain name[s]
port: 5432, // Postgres server port[s]
database: "", // Name of database to connect to
username: "", // Username of database user
password: "", // Password
max: 20, // max connections
})

export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PostgresJSAdapter(sql),
providers: [],
})
```

</Code.Next>
<Code.Qwik>

```ts filename="/src/routes/[email protected]"
import { QwikAuth$ } from "@auth/qwik"
import PostgresJSAdapter from "@auth/pgjs-adapter"
import postgres from "postgres"

const sql = postgres(process.env.PGSQL, {
host: "", // Postgres ip address[s] or domain name[s]
port: 5432, // Postgres server port[s]
database: "", // Name of database to connect to
username: "", // Username of database user
password: "", // Password
max: 20, // max connections
})

export const { onRequest, useSession, useSignIn, useSignOut } = QwikAuth$(
() => ({
providers: [],
adapter: PostgresJSAdapter(sql),
})
)
```

</Code.Qwik>
<Code.Svelte>

```ts filename="./src/auth.ts"
import { SvelteKitAuth } from "@auth/sveltekit"
import PostgresJSAdapter from "@auth/pgjs-adapter"
import postgres from "postgres"

const sql = postgres(process.env.PGSQL, {
host: "", // Postgres ip address[s] or domain name[s]
port: 5432, // Postgres server port[s]
database: "", // Name of database to connect to
username: "", // Username of database user
password: "", // Password
max: 20, // max connections
})

export const { handle, signIn, signOut } = SvelteKitAuth({
adapter: PostgresJSAdapter(sql),
providers: [],
})
```

</Code.Svelte>
<Code.Express>

```ts filename="./src/routes/auth.route.ts"
import { ExpressAuth } from "@auth/express"
import PostgresJSAdapter from "@auth/pgjs-adapter"
import postgres from "postgres"

const sql = postgres(process.env.PGSQL, {
host: "", // Postgres ip address[s] or domain name[s]
port: 5432, // Postgres server port[s]
database: "", // Name of database to connect to
username: "", // Username of database user
password: "", // Password
max: 20, // max connections
})

const app = express()

app.set("trust proxy", true)
app.use(
"/auth/*",
ExpressAuth({
providers: [],
adapter: PostgresJSAdapter(sql),
})
)
```

</Code.Express>
</Code>

### Schema

The SQL schema for the tables used by this adapter is as follows. Learn more about the models at our
doc page on [Database Models](/guides/creating-a-database-adapter).

<Callout type="info">
For `userId`, this schema uses [nanoid](https://github.com/viascom/nanoid-postgres) server-side.
This is **entirely optional** and merely to showcase an alternative ID implementation.
</Callout>

```sql filename="./schema.sql"
--Credit [Supabase](https://supabase.com/docs/guides/database/postgres/dropping-all-tables-in-schema)
do $$ declare
r record;
begin
for r in (select tablename from pg_tables where schemaname = 'public') loop
execute 'drop table if exists ' || quote_ident(r.tablename) || ' cascade';
end loop;
end $$;

--Credit [Nikola Stanković](https://github.com/viascom/nanoid-postgres)
CREATE EXTENSION IF NOT EXISTS pgcrypto;
DROP FUNCTION IF EXISTS nanoid(int, text, float);
CREATE OR REPLACE FUNCTION nanoid(
size int DEFAULT 7, -- The number of symbols in the NanoId String. Must be greater than 0.
alphabet text DEFAULT '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', -- The symbols used in the NanoId String. Must contain between 1 and 255 symbols.
additionalBytesFactor float DEFAULT 1.02 -- The additional bytes factor used for calculating the step size. Must be equal or greater then 1.
)
RETURNS text -- A randomly generated NanoId String
LANGUAGE plpgsql
VOLATILE
PARALLEL SAFE
-- LEAKPROOF // This option requires superuser privileges

AS
$$
DECLARE
alphabetArray text[];
alphabetLength int := 64;
mask int := 63;
step int := 34;
BEGIN
IF size IS NULL OR size < 1 THEN
RAISE EXCEPTION 'The size must be defined and greater than 0!';
END IF;

IF alphabet IS NULL OR length(alphabet) = 0 OR length(alphabet) > 255 THEN
RAISE EXCEPTION 'The alphabet can''t be undefined, zero or bigger than 255 symbols!';
END IF;

IF additionalBytesFactor IS NULL OR additionalBytesFactor < 1 THEN
RAISE EXCEPTION 'The additional bytes factor can''t be less than 1!';
END IF;

alphabetArray := regexp_split_to_array(alphabet, '');
alphabetLength := array_length(alphabetArray, 1);
mask := (2 << cast(floor(log(alphabetLength - 1) / log(2)) as int)) - 1;
step := cast(ceil(additionalBytesFactor * mask * size / alphabetLength) AS int);

IF step > 1024 THEN
step := 1024; -- The step size % can''t be bigger then 1024!
END IF;

RETURN nanoid_optimized(size, alphabet, mask, step);
END
$$;

-- Generates an optimized random string of a specified size using the given alphabet, mask, and step.
-- This optimized version is designed for higher performance and lower memory overhead.
-- No checks are performed! Use it only if you really know what you are doing.
DROP FUNCTION IF EXISTS nanoid_optimized(int, text, int, int);
CREATE OR REPLACE FUNCTION nanoid_optimized(
size int, -- The desired length of the generated string.
alphabet text, -- The set of characters to choose from for generating the string.
mask int, -- The mask used for mapping random bytes to alphabet indices. Should be `(2^n) - 1` where `n` is a power of 2 less than or equal to the alphabet size.
step int -- The number of random bytes to generate in each iteration. A larger value may speed up the function but increase memory usage.
)
RETURNS text -- A randomly generated NanoId String
LANGUAGE plpgsql
VOLATILE
PARALLEL SAFE
-- LEAKPROOF // This option requires superuser privileges
AS
$$
DECLARE
idBuilder text := '';
counter int := 0;
bytes bytea;
alphabetIndex int;
alphabetArray text[];
alphabetLength int := 64;
BEGIN
alphabetArray := regexp_split_to_array(alphabet, '');
alphabetLength := array_length(alphabetArray, 1);

LOOP
bytes := gen_random_bytes(step);
FOR counter IN 0..step - 1
LOOP
alphabetIndex := (get_byte(bytes, counter) & mask) + 1;
IF alphabetIndex <= alphabetLength THEN
idBuilder := idBuilder || alphabetArray[alphabetIndex];
IF length(idBuilder) = size THEN
RETURN idBuilder;
END IF;
END IF;
END LOOP;
END LOOP;
END
$$;
---

CREATE TABLE users
(
id TEXT NOT NULL DEFAULT nanoid(),
---id TEXT NOT NULL, --- Auth.js default - UUID's generated client-side.
---id SERIAL NOT NULL,--- Auto-Incrementing Ids - Dont forget to change "userId" ForeignKeys to INTEGER
name TEXT,
email TEXT,
"emailVerified" TIMESTAMPTZ,
image TEXT,

PRIMARY KEY(id)

);

CREATE TABLE verification_token
(
identifier TEXT NOT NULL,
expires TIMESTAMPTZ NOT NULL,
token TEXT NOT NULL,

PRIMARY KEY (identifier, token)
);

CREATE TABLE accounts
(
"userId" TEXT NOT NULL REFERENCES users(id) ON UPDATE no action ON DELETE cascade,
type TEXT NOT NULL,
provider TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
refresh_token TEXT,
access_token TEXT,
expires_at INT,
id_token TEXT,
scope TEXT,
session_state TEXT,
token_type TEXT,

PRIMARY KEY (provider, "providerAccountId")
);

CREATE TABLE sessions
(
"userId" TEXT NOT NULL REFERENCES users(id) ON UPDATE no action ON DELETE cascade,
expires TIMESTAMPTZ NOT NULL,
"sessionToken" TEXT NOT NULL,

PRIMARY KEY ("sessionToken")
);

create table authenticators
(
id text not null,
"credentialID" text unique not null,
"userId" TEXT NOT NULL REFERENCES users(id) ON UPDATE no action ON DELETE cascade,
"providerAccountId" text not null,
"credentialPublicKey" text not null,
counter integer not null,
"credentialDeviceType" text not null,
"credentialBackedUp" boolean not null,
transports text,

primary key (id)

);
```
16 changes: 16 additions & 0 deletions docs/public/img/adapters/pgjs.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading