skills/vibe-rn-supabase/SKILL.md
Wire Supabase JS client into a React Native (Expo) vibe-kit project: session persistence via AsyncStorage, magic-link OAuth callback via expo-linking deep links, Realtime subscriptions on RN, and shared TypeScript types with the Next.js webapp twin (vibe-kit's typical web<->mobile pair pattern). This is the mobile counterpart of `auth-magic-link` (web). User-visible strings match the user's input language (Vietnamese by default for VN users). Trigger phrases (EN + VN): "supabase react native", "supabase mobile", "auth mobile expo", "magic link mobile", "tich hop supabase vao app", "supabase deep link".
npx skillsauth add Hikkywannafly/vibe-kit vibe-rn-supabaseInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Bridge a React Native (Expo) app to a Supabase backend the way vibe-kit's Next.js webapp does it — same project, same RLS policies, shared types — but with mobile-specific glue (AsyncStorage, deep links, no next/headers).
Language rule: Detect the user's input language. Reply 100% Vietnamese for VN users (Southern, friendly), English for EN. Default Vietnamese for ambiguous input. Code/CLI commands stay English.
@supabase/supabase-js v2.45+@react-native-async-storage/async-storage for session persistenceexpo-linking + expo-web-browser for OAuth/magic-link callbacksexpo-tailwind-setup skill)If user is missing any of the above, invoke expo-tailwind-setup or instruct them to run supabase-setup skill first to provision the backend.
npx expo install @supabase/supabase-js @react-native-async-storage/async-storage expo-linking expo-web-browser
Why each:
@supabase/supabase-js — universal client (works in RN with adapter)@react-native-async-storage/async-storage — persists JWT session across app restartsexpo-linking — handles myapp:// deep-link callbacks from Supabase auth emailsexpo-web-browser — opens magic-link / OAuth in in-app browser, returns to app via deep linkEdit app.json (or app.config.ts):
{
"expo": {
"scheme": "vibekitapp",
"ios": { "bundleIdentifier": "com.vibekit.<project-slug>" },
"android": { "package": "com.vibekit.<projectslug>" }
}
}
Replace vibekitapp with the project's slug from .vibe/intent.json. The scheme MUST be unique per app — collisions cause silent deep-link failures.
lib/supabase.ts)import 'react-native-url-polyfill/auto'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false, // RN does NOT use URLs for callbacks — deep links instead
},
})
Then npx expo install react-native-url-polyfill (Supabase needs URL APIs missing in RN's Hermes runtime).
.env keys (note EXPO_PUBLIC_* prefix — Expo equivalent of NEXT_PUBLIC_*):
EXPO_PUBLIC_SUPABASE_URL=https://<ref>.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...
HARD RULE: NEVER add SUPABASE_SERVICE_ROLE_KEY to a mobile app. Mobile bundles ship to user devices = anyone can extract. Service-role calls go through your Next.js backend, which the mobile app calls via authenticated API.
app/(auth)/sign-in.tsx:
import { useState } from 'react'
import { View, Text, TextInput, Pressable, Alert } from 'react-native'
import * as Linking from 'expo-linking'
import { supabase } from '@/lib/supabase'
export default function SignIn() {
const [email, setEmail] = useState('')
const [loading, setLoading] = useState(false)
const sendMagicLink = async () => {
setLoading(true)
const redirectTo = Linking.createURL('/auth/callback')
const { error } = await supabase.auth.signInWithOtp({
email,
options: { emailRedirectTo: redirectTo },
})
setLoading(false)
if (error) Alert.alert('Lỗi', error.message)
else Alert.alert('Đã gửi', 'Mở email để bấm vào link đăng nhập.')
}
return (
<View className="flex-1 justify-center px-6 bg-white dark:bg-black">
<Text className="text-2xl font-bold mb-4 text-black dark:text-white">Đăng nhập</Text>
<TextInput
className="border border-gray-300 dark:border-gray-700 rounded-lg p-3 mb-4 text-black dark:text-white"
placeholder="[email protected]"
autoCapitalize="none"
autoComplete="email"
keyboardType="email-address"
value={email}
onChangeText={setEmail}
/>
<Pressable
className="bg-blue-600 rounded-lg p-3 items-center"
onPress={sendMagicLink}
disabled={loading || !email}
>
<Text className="text-white font-semibold">
{loading ? 'Đang gửi...' : 'Gửi link đăng nhập'}
</Text>
</Pressable>
</View>
)
}
(VN labels by default; switch to English when project is EN.)
app/auth/callback.tsx:
import { useEffect } from 'react'
import { useLocalSearchParams, router } from 'expo-router'
import { supabase } from '@/lib/supabase'
export default function AuthCallback() {
const params = useLocalSearchParams<{ access_token?: string; refresh_token?: string }>()
useEffect(() => {
if (params.access_token && params.refresh_token) {
supabase.auth.setSession({
access_token: params.access_token,
refresh_token: params.refresh_token,
}).then(() => router.replace('/(tabs)'))
}
}, [params])
return null
}
In Supabase dashboard → Authentication → URL Configuration, add the redirect URL pattern: vibekitapp://auth/callback (replace vibekitapp with your scheme).
hooks/useSession.ts:
import { useEffect, useState } from 'react'
import type { Session } from '@supabase/supabase-js'
import { supabase } from '@/lib/supabase'
export function useSession() {
const [session, setSession] = useState<Session | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
supabase.auth.getSession().then(({ data }) => {
setSession(data.session)
setLoading(false)
})
const { data: sub } = supabase.auth.onAuthStateChange((_event, sess) => {
setSession(sess)
})
return () => sub.subscription.unsubscribe()
}, [])
return { session, loading, user: session?.user ?? null }
}
Use in protected screens: const { session, loading } = useSession(); if (!session) return <Redirect href="/(auth)/sign-in" />;
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'
export function useRealtimeMessages(chatId: string) {
const [messages, setMessages] = useState<Message[]>([])
useEffect(() => {
const channel = supabase
.channel(`chat:${chatId}`)
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `chat_id=eq.${chatId}`,
}, (payload) => setMessages(prev => [...prev, payload.new as Message]))
.subscribe()
return () => { supabase.removeChannel(channel) }
}, [chatId])
return messages
}
Identical API to web — mobile RN client speaks same Realtime protocol. RLS policies apply automatically (user can only subscribe to channels their JWT permits).
If user has both web (farm-management-saas-fe) and mobile (farm-management-saas-mobile) repos:
supabase gen types typescript --project-id <ref> > lib/supabase/types.ts in ONE repo (web), commit.lib/supabase/types.ts from web (do NOT symlink — Metro bundler hates symlinks crossing repos).README.md: "When DB schema changes, regen on web first, then copy types to mobile."For shared business logic (validators, helpers), recommend extracting to a separate npm package later. For an MVP port, copy-paste is fine.
| Pitfall | Why it fails | Fix |
|---|---|---|
| Forgetting react-native-url-polyfill/auto import | Hermes runtime lacks URL constructor | Import at top of lib/supabase.ts |
| Using localStorage (web pattern) | Doesn't exist in RN | Use AsyncStorage adapter |
| detectSessionInUrl: true | RN has no URL bar; never fires | Set to false |
| Service role key in mobile env | Anyone can extract from APK/IPA bundle | NEVER include; route through web backend |
| Same scheme as another app on device | Deep-link goes to wrong app | Use unique scheme like vibekitapp-<slug> |
| Forgot to register redirect URL in Supabase dashboard | Magic link returns 400 | Add myscheme://auth/callback to allowed redirect URLs |
After running this skill, fullstack-dev should have:
lib/supabase.ts — clientapp/(auth)/sign-in.tsx — magic-link screenapp/auth/callback.tsx — deep-link handlerhooks/useSession.ts — auth state hookhooks/useRealtimeMessages.ts if Realtime neededapp.json — scheme field set.env.example — EXPO_PUBLIC_SUPABASE_URL, EXPO_PUBLIC_SUPABASE_ANON_KEYStatus line for orchestrator: "Supabase RN wired. Auth + Realtime ready. Scheme: vibekitapp."
data-ai
Generate Vietnamese marketing copy, UI strings, CTAs, error messages, and email templates for vibe-kit projects. Tone: friendly, conversational, Southern Vietnamese style. Activated for any user-visible text generation.
development
One-shot orchestrator. Turns the prose after /vibe into a shipped product by clarifying intent, rendering a plan, gating on approval, then spawning planner+researcher+fullstack-dev+tester+reviewer agents in sequence. User-visible strings match the user's input language (Vietnamese by default for VN users). Two modes: SAFE (default — clarify + show plan + wait for approval, max 1 round-trip) and YOLO (skip clarify+approval, run full auto with smart defaults — for demos and power users). YOLO triggers: prose contains `yolo`, `nhanh nha`, `lam luon`, `khoi hoi`, `auto`, or args start with `yolo`. Trigger phrases (EN + VN): "build me a site", "make me a landing page", "create a shop", "I need an app", "vibe lam website", "tao cho toi mot", "xay dung shop online", "lam landing page", "can mot app".
tools
On-demand security audit for vibe-kit projects. Stack-aware checks for Next.js App Router + Supabase + Polar: secrets leak, RLS gaps, service-role key in client bundle, missing webhook signature verification, unprotected API routes, weak headers, dependency vulns. Outputs a Vietnamese P0/P1/P2 report with file:line + fix hints. User-visible strings match the user's input language (Vietnamese by default for VN users). Trigger phrases (EN + VN): "check security", "audit it", "security scan", "is this safe to launch", "kiem tra bao mat", "quet bao mat", "audit du an", "co an toan khong", "scan bao mat truoc khi deploy".
data-ai
Scaffold a new vibe-kit project from a named preset. Loads presets/<name>.md, injects pre-filled intent into /vibe context, skips classification (Step 1), jumps straight to clarify (Step 2) with preset questions. User-visible strings match the user's input language (Vietnamese by default for VN users). Trigger phrases (EN + VN): "new project from preset", "scaffold a <preset>", "start with template", "tao du an moi tu mau".