Skip to main content

Email & Password Auth

pgStack provides email/password authentication with argon2id password hashing, JWT access tokens, and rotating refresh tokens.

SDK usage

Install the SDK:

npm install @pgstack/sdk

Initialize the client:

import { createClient } from '@pgstack/sdk/pgstack';

const pgstack = createClient(
'http://127.0.0.1:8080',
process.env.NEXT_PUBLIC_PGSTACK_ANON_KEY!,
);

createClient(url, anonKey, options?) accepts an optional third argument:

OptionDefaultDescription
headersCustom headers included in every request
autoRefreshTokentrueRefresh the JWT 60 seconds before it expires
persistSessiontrue (browser)Persist the session to sessionStorage (tab-scoped — closing the browser logs the user out). Set false to keep the session in memory only

Sign up

const { data, error } = await pgstack.auth.signUp({
email: 'user@example.com',
password: 'strongpassword123',
});

if (error) {
console.error('Signup failed:', error.message);
} else {
const { user, session } = data!;
console.log('User created:', user.id);
console.log('Access token:', session.access_token);
}

You can include extra user metadata at signup:

const { data, error } = await pgstack.auth.signUp({
email: 'user@example.com',
password: 'strongpassword123',
options: {
data: {
full_name: 'Jane Doe',
avatar_url: 'https://example.com/avatar.jpg',
},
},
});

Sign in

const { data, error } = await pgstack.auth.signInWithPassword({
email: 'user@example.com',
password: 'strongpassword123',
});

if (error) {
console.error('Login failed:', error.message);
} else {
const { user, session } = data!;
// Store session.access_token for subsequent requests
}

Get current user

const { data: { user }, error } = await pgstack.auth.getUser();

if (user) {
console.log(user.id); // UUID
console.log(user.email); // string
console.log(user.role); // 'authenticated'
console.log(user.user_metadata); // { full_name, ... }
console.log(user.created_at);
}

Get current session

const { data: { session } } = await pgstack.auth.getSession();

if (session) {
console.log(session.access_token);
console.log(session.refresh_token);
console.log(session.expires_in); // seconds until expiry
console.log(session.user);
}

Update user

// Update email
const { data, error } = await pgstack.auth.updateUser({
email: 'newemail@example.com',
});

// Update password
const { data, error } = await pgstack.auth.updateUser({
password: 'newstrongpassword456',
});

// Update metadata
const { data, error } = await pgstack.auth.updateUser({
data: { full_name: 'Jane Smith' },
});

Sign out

const { error } = await pgstack.auth.signOut();
// Refresh token is invalidated on the server
// Access token expires naturally (short-lived)

Listen for auth state changes

const { data: { subscription } } = pgstack.auth.onAuthStateChange(
(event, session) => {
switch (event) {
case 'SIGNED_IN':
console.log('User signed in:', session?.user.email);
break;
case 'SIGNED_OUT':
console.log('User signed out');
break;
case 'TOKEN_REFRESHED':
console.log('Token refreshed');
break;
case 'USER_UPDATED':
console.log('User updated:', session?.user);
break;
}
}
);

// Unsubscribe when done
subscription.unsubscribe();

Token refresh

The SDK automatically refreshes the access token 60 seconds before it expires when autoRefreshToken: true (the default).

You can also refresh manually:

const { data, error } = await pgstack.auth.refreshSession();

React hook

For React applications, use the useAuth hook from @pgstack/sdk/react:

import { useAuth } from '@pgstack/sdk/react';

function App() {
const { user, session, loading, signInWithPassword, signOut } = useAuth(
'http://127.0.0.1:8080',
process.env.NEXT_PUBLIC_PGSTACK_ANON_KEY!,
);

if (loading) return <p>Loading...</p>;

if (!user) {
return <LoginForm onLogin={signInWithPassword} />;
}

return (
<div>
<p>Welcome, {user.email}</p>
<button onClick={signOut}>Sign out</button>
</div>
);
}

The useAuth hook persists the session in sessionStorage and auto-refreshes tokens before expiry. Session storage is scoped to the tab, so closing the browser logs the user out; a stolen XSS payload cannot survive across browser restarts. For a "remember me" UX, layer a long-lived refresh token over a regular login flow instead of moving tokens into localStorage.

HTTP API

You can call the auth API directly without the SDK:

Sign up

curl -X POST http://127.0.0.1:8080/auth/v1/signup \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_ANON_KEY' \
-d '{
"email": "user@example.com",
"password": "strongpassword123"
}'

Response:

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"expires_in": 3600,
"token_type": "bearer",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"role": "authenticated",
"user_metadata": {},
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
}

Sign in

curl -X POST 'http://127.0.0.1:8080/auth/v1/token?grant_type=password' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_ANON_KEY' \
-d '{
"grant_type": "password",
"email": "user@example.com",
"password": "strongpassword123"
}'

grant_type must be set in the JSON body — the query string is accepted for Supabase familiarity but ignored by the proxy.

Refresh token

curl -X POST 'http://127.0.0.1:8080/auth/v1/token?grant_type=refresh_token' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_ANON_KEY' \
-d '{"grant_type": "refresh_token", "refresh_token": "YOUR_REFRESH_TOKEN"}'

Get user

curl http://127.0.0.1:8080/auth/v1/user \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

Error handling

All auth errors are returned as {"error": "<message>"} — human-readable strings, not machine codes.

HTTP StatusError messageCause
400invalid request bodyMissing or malformed JSON
400invalid email addressEmail fails validation
409email already registeredSignup with existing email
401invalid email or passwordWrong email/password
401invalid or expired tokenExpired or invalid JWT
401invalid refresh tokenRefresh token not found
401refresh token expiredRefresh token past expiry
401refresh token reuse detected — please log in againRevoked refresh token replayed (whole family revoked)
403email not verifiedLogin with REQUIRE_EMAIL_VERIFICATION=true before the email is confirmed
429too many requests, try again laterMore than AUTH_RATE_LIMIT (default 30) requests/min per IP on signup/token/recover/magiclink

Password requirements

pgStack enforces a minimum password length of 8 characters and a maximum of 256 characters. You can add your own validation in your application before calling the auth API.