Skip to main content

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)
Why ConsentKeys + 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:

ItemStatusDescription
ConsentKeys Client IDck_xxxxx from Developer Portal
ConsentKeys Client SecretStore securely, never expose
Supabase Project URLhttps://<ref>.supabase.co
Supabase Anon KeyFor frontend client
Supabase Service Role KeyFor Edge Functions only

Step 1: Register OAuth Client

In the ConsentKeys Developer Portal, configure:

SettingValue
Redirect URIhttps://<project-ref>.supabase.co/functions/v1/oauth-callback
Scopesopenid profile email
Response Typecode
Exact Match Required

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 NameValue
CONSENT_KEYS_CLIENT_SECRETYour OAuth client secret
APP_URLYour 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

supabase/config.toml
project_id = "<your-project-ref>"

[functions.oauth-callback]
verify_jwt = false # Must be false - public OAuth callback

3.3 Edge Function

supabase/functions/oauth-callback/index.ts
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 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

RLS policies for profiles
-- 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

Trigger to create profile on signup
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

Template for user-owned 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

src/lib/supabase.ts
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)

src/hooks/useAuth.tsx
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

src/components/LoginButton.tsx
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

src/components/ProtectedRoute.tsx
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:

supabase/config.toml
[functions.oauth-callback]
verify_jwt = false # Public OAuth callback

[functions.my-protected-function]
verify_jwt = true # Requires Supabase JWT

Protected Function Pattern

supabase/functions/my-protected-function/index.ts
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

Calling protected functions
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

StepTestExpected Result
1Click login buttonRedirects to ConsentKeys
2AuthenticateConsent screen appears
3Complete authRedirected back to app
4Check sessionsupabase.auth.getSession() returns user
5Refresh pageStill logged in
6Query databaseOnly own data returned
7LogoutSession 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

ErrorCauseSolution
"Authorization code not found"Redirect URI mismatchVerify exact match in Developer Portal
"Failed to get token"Invalid client secretCheck CONSENT_KEYS_CLIENT_SECRET in Supabase secrets
"Failed to create user"Service role key issueVerify SUPABASE_SERVICE_ROLE_KEY is set
Session not persistingWrong APP_URLSet correct frontend URL in secrets
RLS blocking queriesPolicy mismatchEnsure 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

Never Expose Secrets
  • 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 dashboardCommit secrets to git
Enable RLS on all tablesDisable RLS for convenience
Use verify_jwt = true for protected functionsSkip authentication
Validate all user inputTrust 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