1. Core Principles
The Problem
Supabase Auth does not natively support domestic OAuth platforms like Feishu or WeChat. We need to use these third-party platforms for identity verification while reusing Supabase’s session management capabilities (JWT, refresh token, RLS).The Solution
Treat third-party OAuth as an “access control system” — responsible only for verifying identity, not for managing session tokens. Once identity is verified, Supabase Auth issues the real session token. Data flow:2. user_identities Table Design
Table Structure
Key Design Decisions
1.oauth_provider and oauth_open_id are nullable fields
This is the most important design decision. The reason:
The trigger executes for all2. Use a partial unique index instead of a composite unique constraintauth.usersinserts. If a user registers via email/password, phone number, or other non-OAuth methods,user_metadatawill not containoauth_provideroroauth_open_id. If these two fields areNOT NULL, the trigger would fail with a NOT NULL constraint violation, causing user registration to fail. Therefore, these fields must be nullable, allowing the trigger to create identity records for all user types — OAuth users get specific values, non-OAuth users getNULL.
UNIQUE(oauth_provider, oauth_open_id)?
- In PostgreSQL’s
UNIQUEconstraint,NULLdoes not equalNULL, so multiple rows with(NULL, NULL)won’t conflict - However, the intent is not semantically clear; a partial unique index more explicitly expresses the intent: enforce uniqueness only when OAuth fields are non-null
- This ensures the same user on the same platform is never created twice, while allowing any number of non-OAuth users
id directly references auth.users(id) with ON DELETE CASCADE
user_identities.idandauth.users.idare the same UUID- When a user is deleted from the Supabase dashboard, the profile record is automatically cleaned up
- RLS policies can directly use
auth.uid() = idfor access control
raw_metadata stores platform-specific fields
Different OAuth platforms return different user info fields (Feishu has tenant_key, WeChat has unionid). Rather than adding dedicated columns per platform, storing them all in JSONB means the table structure never needs to change for new platforms.
RLS Policies
3. Trigger
Trigger Function
Bind the Trigger
Trigger Behavior Summary
| Registration Method | oauth_provider | oauth_open_id | raw_metadata |
|---|---|---|---|
| Feishu OAuth | 'feishu' | 'ou_xxxxx' | Contains name, avatar_url, tenant_key, etc. |
| WeChat OAuth | 'wechat' | 'oXXXX' | Contains nickname, headimgurl, unionid, etc. |
| Email/password | NULL | NULL | {} or metadata passed during registration |
| Phone number | NULL | NULL | {} |
SECURITY DEFINER ensures the trigger executes with the creator’s permissions, bypassing RLS to write to the profiles table. NEW.raw_user_meta_data is the user_metadata object passed to Supabase Auth during createUser.
4. Edge Function Key Code (Feishu Example)
Find or Create User
Issue Session Token
Frontend Session Establishment
5. Frontend Auth State
No custom Context or localStorage needed — use Supabase Auth’s built-in session management directly:session.user.user_metadata — the same userMetadata object passed during createUser.
6. Extending to New OAuth Platforms
To add WeChat as an example, you only need to:- Create a new Edge Function
wechat-auth: implement WeChat OAuth authorization code exchange and user info retrieval - Same Supabase logic: query
user_identities→ create/update user →generateLink→ returntoken_hash - Change the provider on lookup:
.eq('oauth_provider', 'wechat').eq('oauth_open_id', wechatOpenId)
user_identities table structure, trigger, frontend useAuth, verifyOtp logic, RLS policies — all reused.
7. Common Pitfalls
| Issue | Cause | Solution |
|---|---|---|
| Non-OAuth user registration fails with trigger error | oauth_provider and oauth_open_id set as NOT NULL, but non-OAuth users don’t have these fields in metadata | Make them nullable; trigger handles all cases: fills in OAuth info when present, leaves NULL otherwise |
| Partial unique index vs. composite unique constraint | UNIQUE(a, b) in PostgreSQL allows multiple (NULL, NULL) rows (because NULL != NULL), which is semantically unclear | Use CREATE UNIQUE INDEX ... WHERE ... IS NOT NULL partial index to explicitly constrain only non-null values |
generateLink returns a one-time hashed_token | After verifyOtp succeeds, the token is invalidated | Frontend must call verifyOtp immediately on the callback page; cannot delay or retry |
createUser in Edge Function requires email | Supabase Auth requires each user to have an email | Use virtual email {provider}_{openid}@oauth.local; do not use the real email from the OAuth platform to avoid conflicts with regular accounts |

