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.
Starter repository
You can get started quickly by cloning the React + Supabase starter and configuring your ConsentKeys client and Supabase project:
react-supabase-starter-code — clone, set your client variables and Supabase project, and run.
After cloning, add your ConsentKeys Client ID/Secret and Supabase credentials (see Prerequisites and Step 2: Configure Supabase Secrets below). The repo includes the Edge Function, auth context, protected routes, and a simple dashboard.
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 = "https://api.consentkeys.com/token";
const USER_INFO_URL = "https://api.consentkeys.com/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 = `https://api.consentkeys.com/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