Supabase Integration
Integrate ConsentKeys authentication into applications using Supabase as your backend. This guide shows how to bridge ConsentKeys OAuth with Supabase Auth using Edge Functions.
Prerequisites
- A ConsentKeys Client ID and Secret from the Developer Portal
- A Supabase project (supabase.com)
- Supabase CLI installed (
npm install -g supabase)
ConsentKeys provides privacy-first, pseudonymous authentication while Supabase offers a complete backend with PostgreSQL, Row Level Security, and Edge Functions. Together, they create a powerful, privacy-respecting application stack.
Architecture Overview
User Flow Visualization
Quick Start Checklist
Before starting, ensure you have:
| Item | Status | Description |
|---|---|---|
| ConsentKeys Client ID | ☐ | ck_xxxxx from Developer Portal |
| ConsentKeys Client Secret | ☐ | Store securely, never expose |
| Supabase Project URL | ☐ | https://<ref>.supabase.co |
| Supabase Anon Key | ☐ | For frontend client |
| Supabase Service Role Key | ☐ | For Edge Functions only |
Step 1: Register OAuth Client
In the ConsentKeys Developer Portal, configure:
| Setting | Value |
|---|---|
| Redirect URI | https://<project-ref>.supabase.co/functions/v1/oauth-callback |
| Scopes | openid profile email |
| Response Type | code |
The redirect URI must match exactly, including trailing slashes. Copy-paste to avoid errors.
Step 2: Configure Supabase Secrets
In your Supabase dashboard, go to Settings → Edge Functions → Secrets and add:
| Secret Name | Value |
|---|---|
CONSENT_KEYS_CLIENT_SECRET | Your OAuth client secret |
APP_URL | Your frontend URL (e.g., https://yourapp.com) |
Step 3: Create OAuth Callback Edge Function
3.1 Project Structure
your-project/
├── supabase/
│ ├── config.toml
│ └── functions/
│ └── oauth-callback/
│ └── index.ts
└── src/
└── ...
3.2 Configuration
project_id = "<your-project-ref>"
[functions.oauth-callback]
verify_jwt = false # Must be false - public OAuth callback
3.3 Edge Function
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
// ConsentKeys endpoints
const TOKEN_URL = "http://pseudoidc.consentkeys.com:3000/token";
const USER_INFO_URL = "http://pseudoidc.consentkeys.com:3000/userinfo";
// Your OAuth client ID
const CLIENT_ID = "ck_your_client_id";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
try {
// Initialize Supabase Admin client
const supabaseAdmin = createClient(
Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
);
// 1. Get authorization code
const url = new URL(req.url);
const code = url.searchParams.get("code");
if (!code) {
return new Response("Authorization code not found", { status: 400 });
}
// 2. Exchange code for access token
const redirectUri = `${Deno.env.get("SUPABASE_URL")}/functions/v1/oauth-callback`;
const tokenResponse = await fetch(TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
code,
client_id: CLIENT_ID,
client_secret: Deno.env.get("CONSENT_KEYS_CLIENT_SECRET") ?? "",
redirect_uri: redirectUri,
grant_type: "authorization_code",
}),
});
if (!tokenResponse.ok) {
console.error("Token exchange failed:", await tokenResponse.text());
return new Response("Failed to get token", { status: 500 });
}
const tokenData = await tokenResponse.json();
// 3. Fetch user info
const userResponse = await fetch(USER_INFO_URL, {
headers: { Authorization: `Bearer ${tokenData.access_token}` },
});
if (!userResponse.ok) {
return new Response("Failed to get user info", { status: 500 });
}
const providerUser = await userResponse.json();
// 4. Create or find user in Supabase Auth
const { error: userError } = await supabaseAdmin.auth.admin.createUser({
email: providerUser.email,
email_confirm: true,
user_metadata: {
provider: "consent_keys",
provider_id: providerUser.sub,
display_name: providerUser.name || providerUser.email?.split("@")[0],
},
});
// Ignore "email_exists" error - user already exists
if (userError && userError.code !== "email_exists" &&
!userError.message?.includes("already registered")) {
console.error("User creation failed:", userError);
return new Response("Failed to create user", { status: 500 });
}
// 5. Generate magic link for session
const { data: linkData, error: linkError } = await supabaseAdmin.auth.admin.generateLink({
type: "magiclink",
email: providerUser.email,
options: {
redirectTo: Deno.env.get("APP_URL") ?? "https://yourapp.com",
},
});
if (linkError || !linkData?.properties?.action_link) {
console.error("Magic link generation failed:", linkError);
return new Response("Failed to generate session", { status: 500 });
}
// 6. Redirect to magic link (creates session)
return new Response(null, {
status: 303,
headers: { Location: linkData.properties.action_link },
});
} catch (error) {
console.error("OAuth callback error:", error);
return new Response(
JSON.stringify({ error: error instanceof Error ? error.message : "Unknown error" }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
});
3.4 Deploy
# Login and link project
supabase login
supabase link --project-ref <your-project-ref>
# Deploy function
supabase functions deploy oauth-callback --no-verify-jwt
Step 4: Database Schema & RLS
4.1 Profiles Table
CREATE TABLE public.profiles (
id uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid NOT NULL UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE,
display_name text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
4.2 Row Level Security Policies
-- Users can only view their own profile
CREATE POLICY "Users can view their own profile"
ON public.profiles FOR SELECT
USING (auth.uid() = user_id);
-- Users can update their own profile
CREATE POLICY "Users can update their own profile"
ON public.profiles FOR UPDATE
USING (auth.uid() = user_id);
-- Users can insert their own profile
CREATE POLICY "Users can insert their own profile"
ON public.profiles FOR INSERT
WITH CHECK (auth.uid() = user_id);
4.3 Auto-Create Profile Trigger
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
INSERT INTO public.profiles (user_id, display_name)
VALUES (
NEW.id,
COALESCE(NEW.raw_user_meta_data->>'display_name', NEW.email)
);
RETURN NEW;
END;
$$;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_user();
4.4 RLS Pattern for Your Tables
-- Create table with user_id foreign key
CREATE TABLE public.your_table (
id uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
-- your columns
created_at timestamptz NOT NULL DEFAULT now()
);
-- Enable RLS
ALTER TABLE public.your_table ENABLE ROW LEVEL SECURITY;
-- Standard policies
CREATE POLICY "Users can view their own data"
ON public.your_table FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert their own data"
ON public.your_table FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update their own data"
ON public.your_table FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Users can delete their own data"
ON public.your_table FOR DELETE USING (auth.uid() = user_id);
Step 5: Frontend Integration
5.1 Supabase Client
import { createClient } from "@supabase/supabase-js";
const SUPABASE_URL = "https://<project-ref>.supabase.co";
const SUPABASE_ANON_KEY = "eyJ..."; // Your anon key
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
storage: localStorage,
persistSession: true,
autoRefreshToken: true,
},
});
5.2 Auth Hook (React)
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
import { User, Session } from "@supabase/supabase-js";
import { supabase } from "@/lib/supabase";
interface AuthContextType {
user: User | null;
session: Session | null;
loading: boolean;
signOut: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Listen for auth changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
setSession(session);
setUser(session?.user ?? null);
setLoading(false);
}
);
// Check existing session
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
setUser(session?.user ?? null);
setLoading(false);
});
return () => subscription.unsubscribe();
}, []);
const signOut = async () => {
await supabase.auth.signOut();
};
return (
<AuthContext.Provider value={{ user, session, loading, signOut }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within AuthProvider");
}
return context;
}
5.3 Login Button
const CLIENT_ID = "ck_your_client_id";
const SUPABASE_PROJECT_REF = "<your-project-ref>";
export function LoginButton() {
const signInWithConsentKeys = () => {
const params = new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: `https://${SUPABASE_PROJECT_REF}.supabase.co/functions/v1/oauth-callback`,
response_type: "code",
scope: "openid profile email",
});
window.location.href = `http://pseudoidc.consentkeys.com:3000/auth?${params}`;
};
return (
<button onClick={signInWithConsentKeys}>
Sign in with ConsentKeys
</button>
);
}
5.4 Protected Route
import { Navigate } from "react-router-dom";
import { useAuth } from "@/hooks/useAuth";
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
if (!user) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
}
Step 6: Protected Edge Functions
For authenticated API endpoints, enable JWT verification:
[functions.oauth-callback]
verify_jwt = false # Public OAuth callback
[functions.my-protected-function]
verify_jwt = true # Requires Supabase JWT
Protected Function Pattern
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
return new Response(
JSON.stringify({ error: 'Missing authorization' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// Client with user's auth - respects RLS!
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{ global: { headers: { Authorization: authHeader } } }
);
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
}
// Your logic here - RLS automatically filters to user's data
const { data } = await supabase.from('your_table').select('*');
return new Response(
JSON.stringify({ data }),
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
);
});
Calling from Frontend
import { supabase } from "@/lib/supabase";
async function callProtectedFunction() {
const { data: { session } } = await supabase.auth.getSession();
const response = await fetch(
"https://<project-ref>.supabase.co/functions/v1/my-protected-function",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${session?.access_token}`,
},
body: JSON.stringify({ /* data */ }),
}
);
return response.json();
}
Testing Your Integration
Test Checklist
| Step | Test | Expected Result |
|---|---|---|
| 1 | Click login button | Redirects to ConsentKeys |
| 2 | Authenticate | Consent screen appears |
| 3 | Complete auth | Redirected back to app |
| 4 | Check session | supabase.auth.getSession() returns user |
| 5 | Refresh page | Still logged in |
| 6 | Query database | Only own data returned |
| 7 | Logout | Session cleared |
Debug Commands
# View Edge Function logs
supabase functions logs oauth-callback
# Test RLS in SQL Editor
SELECT * FROM your_table; -- Should only show your rows
Troubleshooting
Common Errors
| Error | Cause | Solution |
|---|---|---|
| "Authorization code not found" | Redirect URI mismatch | Verify exact match in Developer Portal |
| "Failed to get token" | Invalid client secret | Check CONSENT_KEYS_CLIENT_SECRET in Supabase secrets |
| "Failed to create user" | Service role key issue | Verify SUPABASE_SERVICE_ROLE_KEY is set |
| Session not persisting | Wrong APP_URL | Set correct frontend URL in secrets |
| RLS blocking queries | Policy mismatch | Ensure user_id matches auth.uid() |
Debug Logging
Add console logs to your Edge Function:
console.log("1. Code received:", code ? "yes" : "no");
console.log("2. Token exchange:", tokenResponse.ok ? "success" : "failed");
console.log("3. User email:", providerUser.email);
View logs at: Supabase Dashboard → Edge Functions → Logs
Security Best Practices
- Client secret → Only in Edge Function secrets
- Service role key → Never in frontend code
- Use anon key in frontend clients
| Do ✅ | Don't ❌ |
|---|---|
| Store secrets in Supabase dashboard | Commit secrets to git |
| Enable RLS on all tables | Disable RLS for convenience |
Use verify_jwt = true for protected functions | Skip authentication |
| Validate all user input | Trust client data |
Complete Example
See the PersonaScope AI project for a full working implementation with:
- ConsentKeys OAuth via Supabase Edge Function
- Multiple protected Edge Functions
- Full RLS implementation
- React frontend with auth context
Next Steps
- Learn about OAuth concepts
- Understand magic link authentication
- Explore the API reference