A complete tutorial demonstrating how to integrate Authgear authentication with Supabase, including JWT token exchange and Row Level Security (RLS).
This project demonstrates a simple instrument tracking app where users can manage their personal collection after logging in with Authgear.
This project shows how to:
- Use Authgear for user authentication in a React app
- Exchange Authgear JWT tokens for Supabase JWTs
- Implement Row Level Security (RLS) with user-specific data
- Build a CRUD app with per-user data isolation
- Authentication: Authgear OAuth 2.0 with PKCE flow
- Token Exchange: Automatic JWT exchange via Supabase Edge Function
- Database: Supabase PostgreSQL with RLS policies
- Frontend: React + Vite with minimal CSS (browser defaults)
- CRUD Operations: User-specific instrument management
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Browser │ │ Authgear │ │ Supabase │
│ (React App)│◄────────►│ (Auth IdP) │ │ (Database) │
└──────┬──────┘ └──────────────┘ └──────┬───────┘
│ │
│ 1. Login with Authgear │
│ 2. Get Authgear JWT │
│ │
│ 3. Call Supabase with Authgear JWT │
└──────────────────────────────────────────────────►
│ │
│ 4. Exchange JWT (Edge Function) │
│ 5. Return Supabase JWT │
│◄──────────────────────────────────────────────────
│ │
│ 6. Access data with Supabase JWT │
└──────────────────────────────────────────────────►
- Node.js 18+ and npm
- Supabase account and project
- Authgear account and project
- Supabase CLI (for deploying functions)
git clone <repository-url>
cd authgear-supabase-demo
cd my-app
npm installRun this SQL in your Supabase SQL Editor:
-- Create a helper function to get the current user ID from JWT
CREATE OR REPLACE FUNCTION current_user_id()
RETURNS TEXT AS $$
SELECT auth.jwt() ->> 'sub';
$$ LANGUAGE SQL STABLE;
-- Create instruments table
CREATE TABLE instruments (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,
name TEXT NOT NULL,
user_id TEXT NOT NULL
);
-- Enable Row Level Security
ALTER TABLE instruments ENABLE ROW LEVEL SECURITY;
-- Create RLS policies for user-specific access
CREATE POLICY "users can read their own instruments"
ON "public"."instruments"
FOR SELECT
TO authenticated
USING (user_id = current_user_id());
CREATE POLICY "users can insert their own instruments"
ON "public"."instruments"
FOR INSERT
TO authenticated
WITH CHECK (user_id = current_user_id());
CREATE POLICY "users can update their own instruments"
ON "public"."instruments"
FOR UPDATE
TO authenticated
USING (user_id = current_user_id())
WITH CHECK (user_id = current_user_id());
CREATE POLICY "users can delete their own instruments"
ON "public"."instruments"
FOR DELETE
TO authenticated
USING (user_id = current_user_id());Key Points:
current_user_id()extracts the user ID from the JWT'ssubclaimuser_idcolumn stores the Authgear user ID- RLS policies use
current_user_id()to ensure users only access their own data - All policies require
authenticatedrole (set by the exchange function)
The JWT exchange function converts Authgear tokens to Supabase tokens.
In your Supabase Dashboard → Edge Functions → Secrets, add:
AUTHGEAR_ENDPOINT=https://your-project.authgear.cloud
SB_JWT_SECRET=your-supabase-jwt-secretFinding your JWT Secret:
- Go to: Supabase Dashboard → Project Settings → API → JWT Secret
- Copy the value (it's different from the anon key!)
cd supabase
npx supabase functions deploy exchange-jwt- Receives Authgear JWT: Client sends Authgear token in
Authorizationheader - Verifies Token: Validates JWT signature using Authgear's public keys (JWKS)
- Extracts Claims: Gets user information (sub, email, etc.) from verified token
- Signs Supabase JWT: Creates new JWT with:
role: "authenticated"(required by Supabase RLS)sub: User ID from Authgear (used in RLS policies)- Other claims from original token
- Returns Token: Client uses this token for Supabase API calls
Function Location: supabase/functions/exchange-jwt/index.ts
Key Code Sections:
// Verify Authgear token with public key
const verified = await verifyToken(token);
// Sign new Supabase JWT
payload.role = "authenticated"; // Required for RLS
const supabaseJwt = await new jose.SignJWT(payload)
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.setIssuer("supabase")
.sign(supabaseSecret);- Go to Authgear Portal
- Create a new application (type: Single Page Application)
- Configure OAuth:
- Redirect URIs:
http://localhost:5173/auth-redirect - Post Logout Redirect URIs:
http://localhost:5173/
- Redirect URIs:
- Note your:
- Authgear Endpoint (e.g.,
https://your-project.authgear.cloud) - Client ID
- Authgear Endpoint (e.g.,
Create my-app/.env.local:
cp my-app/.env.example my-app/.env.localUpdate with your credentials:
# Supabase Configuration
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=your-supabase-anon-key
# Authgear Configuration
VITE_AUTHGEAR_CLIENT_ID=your-authgear-client-id
VITE_AUTHGEAR_ENDPOINT=https://your-project.authgear.cloud
VITE_AUTHGEAR_REDIRECT_URL=http://localhost:5173/auth-redirect
VITE_AUTHGEAR_LOGOUT_REDIRECT_URL=http://localhost:5173/cd my-app
npm run devVisit http://localhost:5173
-
User Clicks Login
- App calls
authgear.startAuthentication() - User redirects to Authgear login page
- App calls
-
User Logs In
- Authgear authenticates user
- Redirects to
/auth-redirectwith auth code
-
Complete Authentication
AuthRedirectcomponent callsauthgear.finishAuthentication()- Exchanges auth code for access token
- Stores token in
authgearSDK - Redirects to home page
-
Session State Management
UserProvidermonitorsauthgear.sessionState- Uses
onSessionStateChangedelegate for real-time updates - Fetches user info when authenticated
-
User Requests Data
- App calls
supabase.from('instruments').select()
- App calls
-
Automatic Token Exchange (via
accessTokencallback)- Supabase client intercepts request
- Calls custom
accessToken()function - Function gets Authgear token and exchanges it:
const response = await fetch(`${SUPABASE_URL}/functions/v1/exchange-jwt`, { headers: { Authorization: `Bearer ${authgearToken}` } }); const { supabaseJwt } = await response.json();
- Returns Supabase JWT to client
-
Database Query with RLS
- Request uses Supabase JWT in
Authorizationheader - Supabase verifies JWT signature
- RLS policies check
auth.jwt() ->> 'sub'matchesuser_id - Returns only user's data
- Request uses Supabase JWT in
The Supabase client is configured with a custom accessToken callback that automatically handles token exchange:
Location: my-app/src/lib/supabase.js
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
autoRefreshToken: false, // We manage tokens via Authgear
persistSession: false, // Session managed by Authgear
},
accessToken: async () => {
// This callback runs before every Supabase API call
await authgear.refreshAccessTokenIfNeeded();
const authgearToken = authgear.accessToken;
if (!authgearToken) return null;
// Exchange Authgear token for Supabase token
const supabaseJwt = await exchangeToken(authgearToken);
return supabaseJwt;
},
});How it works:
- Every Supabase query (SELECT, INSERT, UPDATE, DELETE) triggers the
accessTokencallback - The callback refreshes the Authgear token if needed
- It exchanges the Authgear token for a Supabase JWT via the Edge Function
- The Supabase JWT is automatically added to the request's
Authorizationheader - No manual token management needed in components!
The app caches exchanged tokens to avoid unnecessary API calls:
// Only exchange if Authgear token changed
if (authgearToken === lastAuthgearAccessToken && cachedSupabaseJwt) {
return cachedSupabaseJwt;
}This means the exchange function is only called when:
- The user first logs in
- The Authgear token is refreshed
- The cache is cleared on logout
authgear-supabase-demo/
├── my-app/ # React frontend
│ ├── src/
│ │ ├── components/
│ │ │ ├── Home.jsx # Main app with CRUD
│ │ │ └── AuthRedirect.jsx # OAuth callback handler
│ │ ├── context/
│ │ │ └── UserProvider.jsx # Auth state management
│ │ ├── lib/
│ │ │ └── supabase.js # Supabase client + token exchange
│ │ ├── config/
│ │ │ └── constants.js # Environment variables
│ │ ├── App.jsx # Router setup
│ │ └── main.jsx # Authgear initialization
│ ├── .env.example # Environment template
│ └── package.json
│
└── supabase/ # Supabase backend
└── functions/
└── exchange-jwt/ # JWT exchange function
├── index.ts # Edge Function code
└── .env.example # Function environment template
- Authgear Documentation
- Supabase Documentation
- Supabase RLS Guide
- JWT & JWE Debugger - Decode and inspect JWTs
MIT
