diff --git a/api/index.js b/api/index.js
index db19924..824fd1e 100644
--- a/api/index.js
+++ b/api/index.js
@@ -1,11 +1,65 @@
const express = require('express');
const cors = require('cors');
const { GoogleGenerativeAI } = require('@google/generative-ai');
+const { createClient } = require('@supabase/supabase-js');
const app = express();
app.use(cors());
app.use(express.json());
+const GUEST_WEEKLY_LIMIT = 5;
+const AUTH_WEEKLY_LIMIT = 15;
+const REFERRAL_BONUS = 10;
+
+const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY);
+const supabaseAdmin = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY);
+
+async function upstash(path, method = 'POST') {
+ const res = await fetch(`${process.env.UPSTASH_REDIS_REST_URL}${path}`, {
+ method,
+ headers: { Authorization: `Bearer ${process.env.UPSTASH_REDIS_REST_TOKEN}` },
+ });
+ return res.json();
+}
+
+async function checkRateLimit(ip) {
+ const key = `fmp:rl:${ip}`;
+ const { result: count } = await upstash(`/incr/${key}`);
+ if (count === 1) await upstash(`/expire/${key}/604800`);
+ return count <= GUEST_WEEKLY_LIMIT;
+}
+
+async function checkAuthRateLimit(userId, bonusSearches = 0) {
+ const key = `fmp:rl:user:${userId}`;
+ const { result: count } = await upstash(`/incr/${key}`);
+ if (count === 1) await upstash(`/expire/${key}/604800`);
+ return count <= AUTH_WEEKLY_LIMIT + bonusSearches;
+}
+
+async function getAuthenticatedUser(authHeader) {
+ if (!authHeader?.startsWith('Bearer ')) return null;
+ try {
+ const { data: { user }, error } = await supabase.auth.getUser(authHeader.slice(7));
+ if (error || !user) return null;
+ return user;
+ } catch {
+ return null;
+ }
+}
+
+function generateReferralCode(userId) {
+ return userId.replace(/-/g, '').slice(0, 8);
+}
+
+async function getBonusSearches(userId) {
+ const { data } = await supabase
+ .from('bonus_searches')
+ .select('bonus_count')
+ .eq('user_id', userId)
+ .single();
+ return data?.bonus_count || 0;
+}
+
const gemini = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
const SYSTEM_PROMPT = `You are FindMyPro's AI assistant — a calm, helpful guide that connects people with the right professional. You are NOT a lawyer, doctor, or financial advisor. You help people FIND the right one.
@@ -197,6 +251,23 @@ async function searchWithSerper(query) {
app.post('/api/search', async (req, res) => {
try {
+ const user = await getAuthenticatedUser(req.headers.authorization);
+ if (!user) {
+ const ip = (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
+ || req.socket?.remoteAddress
+ || 'unknown';
+ const allowed = await checkRateLimit(ip);
+ if (!allowed) {
+ return res.status(429).json({ error: 'weekly_limit_reached', limit: GUEST_WEEKLY_LIMIT });
+ }
+ } else {
+ const bonus = await getBonusSearches(user.id);
+ const allowed = await checkAuthRateLimit(user.id, bonus);
+ if (!allowed) {
+ return res.status(429).json({ error: 'weekly_limit_reached', limit: AUTH_WEEKLY_LIMIT + bonus });
+ }
+ }
+
const { queries } = req.body;
const results = await Promise.all(
@@ -213,4 +284,89 @@ app.post('/api/search', async (req, res) => {
}
});
+/* ─── Referral endpoints ───────────────────────────────── */
+
+app.get('/api/referral/info', async (req, res) => {
+ try {
+ const user = await getAuthenticatedUser(req.headers.authorization);
+ if (!user) return res.status(401).json({ error: 'Unauthorized' });
+
+ const referralCode = generateReferralCode(user.id);
+
+ const { data: referrals } = await supabase
+ .from('referrals')
+ .select('id, rewarded, created_at')
+ .eq('referrer_id', user.id);
+
+ const successfulReferrals = (referrals || []).filter(r => r.rewarded).length;
+ const bonusSearches = successfulReferrals * REFERRAL_BONUS;
+
+ res.json({
+ referralCode,
+ referralLink: `https://findmyspecialist.vercel.app?ref=${referralCode}`,
+ totalReferrals: (referrals || []).length,
+ successfulReferrals,
+ bonusSearches,
+ });
+ } catch (error) {
+ console.error('Referral info error:', error);
+ res.status(500).json({ error: 'Failed to fetch referral info' });
+ }
+});
+
+app.post('/api/referral/claim', async (req, res) => {
+ try {
+ const user = await getAuthenticatedUser(req.headers.authorization);
+ if (!user) return res.status(401).json({ error: 'Unauthorized' });
+
+ const { referralCode } = req.body;
+ if (!referralCode) return res.status(400).json({ error: 'No referral code provided' });
+
+ const { data: allUsers } = await supabaseAdmin.auth.admin.listUsers();
+ const referrer = (allUsers?.users || []).find(
+ u => generateReferralCode(u.id) === referralCode
+ );
+
+ if (!referrer) return res.status(400).json({ error: 'Invalid referral code' });
+ if (referrer.id === user.id) return res.status(400).json({ error: 'Cannot refer yourself' });
+
+ const { data: existing } = await supabase
+ .from('referrals')
+ .select('id')
+ .eq('referred_user_id', user.id)
+ .single();
+
+ if (existing) return res.status(400).json({ error: 'Already referred' });
+
+ const { error: insertErr } = await supabase
+ .from('referrals')
+ .insert({ referrer_id: referrer.id, referred_user_id: user.id, rewarded: true });
+
+ if (insertErr) throw insertErr;
+
+ const currentBonus = await getBonusSearches(referrer.id);
+ await supabase
+ .from('bonus_searches')
+ .upsert({ user_id: referrer.id, bonus_count: currentBonus + REFERRAL_BONUS, updated_at: new Date().toISOString() });
+
+ res.json({ success: true });
+ } catch (error) {
+ console.error('Referral claim error:', error);
+ res.status(500).json({ error: 'Failed to process referral' });
+ }
+});
+
+app.get('/api/referral/validate/:code', async (req, res) => {
+ try {
+ const { code } = req.params;
+ const { data: allUsers } = await supabaseAdmin.auth.admin.listUsers();
+ const referrer = (allUsers?.users || []).find(
+ u => generateReferralCode(u.id) === code
+ );
+ res.json({ valid: !!referrer });
+ } catch {
+ res.json({ valid: false });
+ }
+});
+
module.exports = app;
diff --git a/client/src/App.jsx b/client/src/App.jsx
index 27ba527..fa52a05 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -11,7 +11,9 @@ const API_URL = import.meta.env.DEV ? 'http://localhost:3001/api' : '/api'
const STORAGE_KEY = 'findmypro_session';
const SESSIONS_KEY = 'findmypro_sessions';
const USAGE_KEY = 'findmypro_usage';
+const REFERRAL_KEY = 'findmypro_referral';
const WEEKLY_LIMIT = 5;
+const AUTH_WEEKLY_LIMIT = 15;
const SUGGESTIONS = [
{ kind: 'Legal', text: "I got into a car accident in Chicago and my back hurts" },
@@ -641,7 +643,7 @@ function GateModal({ onClose, onShowAuth }) {
You've used your {WEEKLY_LIMIT} free searches this week
- Create a free account for unlimited searches and to save your conversation history across devices.
+ Create a free account for {AUTH_WEEKLY_LIMIT} searches per week (plus bonus searches from referrals) and save your conversation history across devices.
{ onClose(); onShowAuth('signup'); }}>
@@ -657,9 +659,81 @@ function GateModal({ onClose, onShowAuth }) {
);
}
+/* ─── Referral Panel ────────────────────────────────────── */
+
+function ReferralPanel({ session, onClose }) {
+ const [info, setInfo] = useState(null);
+ const [copied, setCopied] = useState(false);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ if (!session) return;
+ fetch(`${API_URL}/referral/info`, {
+ headers: { Authorization: `Bearer ${session.access_token}` },
+ })
+ .then(r => r.json())
+ .then(data => { setInfo(data); setLoading(false); })
+ .catch(() => setLoading(false));
+ }, [session]);
+
+ const copyLink = () => {
+ if (!info?.referralLink) return;
+ navigator.clipboard.writeText(info.referralLink).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ });
+ };
+
+ if (loading) return (
+
+
e.stopPropagation()}>
+
Loading...
+
+
+ );
+
+ return (
+
+
e.stopPropagation()}>
+
+
Refer a Friend
+
+ Share your link — when someone signs up, you get +10 bonus searches/week .
+
+
+
+
+
+ {copied ? 'Copied!' : 'Copy referral link'}
+
+
+
+
+
+ {info?.successfulReferrals || 0}
+ Successful referrals
+
+
+ +{info?.bonusSearches || 0}
+ Bonus searches/week
+
+
+
+
Close
+
+
+ );
+}
+
/* ─── Auth controls ──────────────────────────────────── */
-function AuthControls({ session, onShowAuth, onSignOut }) {
+function AuthControls({ session, onShowAuth, onSignOut, onShowReferral }) {
const [menuOpen, setMenuOpen] = useState(false);
const menuRef = useRef(null);
const name = userName(session);
@@ -686,6 +760,9 @@ function AuthControls({ session, onShowAuth, onSignOut }) {
{menuOpen && (
{session.user.email}
+
{ setMenuOpen(false); onShowReferral(); }}>
+ Refer a Friend
+
{ setMenuOpen(false); onSignOut(); }}>
Sign out
@@ -723,6 +800,7 @@ function App() {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [gateOpen, setGateOpen] = useState(false);
const [authModal, setAuthModal] = useState(null); // null | 'signin' | 'signup'
+ const [referralOpen, setReferralOpen] = useState(false);
const [darkMode, setDarkMode] = useState(() => {
const saved = localStorage.getItem('findmypro_theme');
if (saved) return saved === 'dark';
@@ -734,12 +812,38 @@ function App() {
const resultsRef = useRef(null);
const prevResultsRef = useRef(null);
+ // Capture referral code from URL on mount
+ useEffect(() => {
+ const params = new URLSearchParams(window.location.search);
+ const ref = params.get('ref');
+ if (ref) {
+ localStorage.setItem(REFERRAL_KEY, ref);
+ window.history.replaceState({}, '', window.location.pathname);
+ }
+ }, []);
+
// Supabase auth state
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => setSupaSession(session));
const { data: { subscription } } = supabase.auth.onAuthStateChange((_e, session) => {
setSupaSession(session);
- if (session) setAuthModal(null); // close modal on successful sign-in
+ if (session) {
+ setAuthModal(null);
+ // Claim referral if one exists in localStorage
+ const storedRef = localStorage.getItem(REFERRAL_KEY);
+ if (storedRef) {
+ fetch(`${API_URL}/referral/claim`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${session.access_token}`,
+ },
+ body: JSON.stringify({ referralCode: storedRef }),
+ }).finally(() => {
+ localStorage.removeItem(REFERRAL_KEY);
+ });
+ }
+ }
});
return () => subscription.unsubscribe();
}, []);
@@ -1027,6 +1131,7 @@ function App() {
setReferralOpen(true)}
onSignOut={async () => {
await supabase.auth.signOut();
setSupaSession(null);
@@ -1134,6 +1239,7 @@ function App() {
{authModal && setAuthModal(null)} />}
{gateOpen && setGateOpen(false)} onShowAuth={setAuthModal} />}
+ {referralOpen && setReferralOpen(false)} />}
{toast && {toast}
}
>
);
diff --git a/client/src/styles.css b/client/src/styles.css
index 0a2c06d..79d34e9 100644
--- a/client/src/styles.css
+++ b/client/src/styles.css
@@ -1729,3 +1729,35 @@ body::after {
.about-main { padding: 40px 20px 60px; gap: 48px; }
.about-bio-card { flex-direction: column; }
}
+
+/* ─── Referral panel ─────────────────────────────────── */
+
+.referral-modal {
+ max-width: 420px;
+}
+.referral-link-box {
+ margin: 16px 0;
+ width: 100%;
+}
+.referral-stats {
+ display: flex;
+ gap: 24px;
+ justify-content: center;
+ margin-top: 16px;
+}
+.referral-stat {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+}
+.referral-stat-num {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--accent-deep);
+}
+.referral-stat-label {
+ font-size: 0.78rem;
+ color: var(--ink-3);
+ letter-spacing: 0.02em;
+}
diff --git a/server/index.js b/server/index.js
index 5df81d6..d2afe83 100644
--- a/server/index.js
+++ b/server/index.js
@@ -11,7 +11,9 @@ const app = express();
app.use(cors());
app.use(express.json());
-const WEEKLY_LIMIT = 5;
+const GUEST_WEEKLY_LIMIT = 5;
+const AUTH_WEEKLY_LIMIT = 15;
+const REFERRAL_BONUS = 10;
async function upstash(path, method = 'POST') {
const res = await fetch(`${process.env.UPSTASH_REDIS_REST_URL}${path}`, {
@@ -24,22 +26,49 @@ async function upstash(path, method = 'POST') {
async function checkRateLimit(ip) {
const key = `fmp:rl:${ip}`;
const { result: count } = await upstash(`/incr/${key}`);
- if (count === 1) await upstash(`/expire/${key}/604800`); // 7-day TTL
- return count <= WEEKLY_LIMIT;
+ if (count === 1) await upstash(`/expire/${key}/604800`);
+ return count <= GUEST_WEEKLY_LIMIT;
+}
+
+async function checkAuthRateLimit(userId, bonusSearches = 0) {
+ const key = `fmp:rl:user:${userId}`;
+ const { result: count } = await upstash(`/incr/${key}`);
+ if (count === 1) await upstash(`/expire/${key}/604800`);
+ return count <= AUTH_WEEKLY_LIMIT + bonusSearches;
}
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY);
+const supabaseAdmin = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY);
-async function isAuthenticated(authHeader) {
- if (!authHeader?.startsWith('Bearer ')) return false;
+async function getAuthenticatedUser(authHeader) {
+ if (!authHeader?.startsWith('Bearer ')) return null;
try {
const { data: { user }, error } = await supabase.auth.getUser(authHeader.slice(7));
- return !error && !!user;
+ if (error || !user) return null;
+ return user;
} catch {
- return false;
+ return null;
}
}
+async function isAuthenticated(authHeader) {
+ return !!(await getAuthenticatedUser(authHeader));
+}
+
+function generateReferralCode(userId) {
+ const hash = userId.replace(/-/g, '').slice(0, 8);
+ return hash;
+}
+
+async function getBonusSearches(userId) {
+ const { data } = await supabase
+ .from('bonus_searches')
+ .select('bonus_count')
+ .eq('user_id', userId)
+ .single();
+ return data?.bonus_count || 0;
+}
+
const gemini = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
const SYSTEM_PROMPT = `You are FindMyPro's AI assistant — a calm, helpful guide that connects people with the right professional. You are NOT a lawyer, doctor, or financial advisor. You help people FIND the right one.
@@ -267,14 +296,20 @@ async function searchWithGemini(query) {
app.post('/api/search', async (req, res) => {
try {
- const authed = await isAuthenticated(req.headers.authorization);
- if (!authed) {
+ const user = await getAuthenticatedUser(req.headers.authorization);
+ if (!user) {
const ip = (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
|| req.socket?.remoteAddress
|| 'unknown';
const allowed = await checkRateLimit(ip);
if (!allowed) {
- return res.status(429).json({ error: 'weekly_limit_reached', limit: WEEKLY_LIMIT });
+ return res.status(429).json({ error: 'weekly_limit_reached', limit: GUEST_WEEKLY_LIMIT });
+ }
+ } else {
+ const bonus = await getBonusSearches(user.id);
+ const allowed = await checkAuthRateLimit(user.id, bonus);
+ if (!allowed) {
+ return res.status(429).json({ error: 'weekly_limit_reached', limit: AUTH_WEEKLY_LIMIT + bonus });
}
}
@@ -282,8 +317,6 @@ app.post('/api/search', async (req, res) => {
const results = await Promise.all(
queries.map(async ({ query, label, reason }) => {
- // Use Gemini if USE_GEMINI=true in .env (requires GCP billing enabled)
- // otherwise uses Serper (default)
let items = [];
if (process.env.USE_GEMINI === 'true') {
try {
@@ -307,6 +340,94 @@ app.post('/api/search', async (req, res) => {
}
});
+/* ─── Referral endpoints ───────────────────────────────── */
+
+app.get('/api/referral/info', async (req, res) => {
+ try {
+ const user = await getAuthenticatedUser(req.headers.authorization);
+ if (!user) return res.status(401).json({ error: 'Unauthorized' });
+
+ const referralCode = generateReferralCode(user.id);
+
+ const { data: referrals } = await supabase
+ .from('referrals')
+ .select('id, rewarded, created_at')
+ .eq('referrer_id', user.id);
+
+ const successfulReferrals = (referrals || []).filter(r => r.rewarded).length;
+ const bonusSearches = successfulReferrals * REFERRAL_BONUS;
+
+ res.json({
+ referralCode,
+ referralLink: `https://findmyspecialist.vercel.app?ref=${referralCode}`,
+ totalReferrals: (referrals || []).length,
+ successfulReferrals,
+ bonusSearches,
+ });
+ } catch (error) {
+ console.error('Referral info error:', error);
+ res.status(500).json({ error: 'Failed to fetch referral info' });
+ }
+});
+
+app.post('/api/referral/claim', async (req, res) => {
+ try {
+ const user = await getAuthenticatedUser(req.headers.authorization);
+ if (!user) return res.status(401).json({ error: 'Unauthorized' });
+
+ const { referralCode } = req.body;
+ if (!referralCode) return res.status(400).json({ error: 'No referral code provided' });
+
+ // Find the referrer by their code (first 8 chars of UUID without dashes)
+ const { data: allUsers } = await supabaseAdmin.auth.admin.listUsers();
+ const referrer = (allUsers?.users || []).find(
+ u => generateReferralCode(u.id) === referralCode
+ );
+
+ if (!referrer) return res.status(400).json({ error: 'Invalid referral code' });
+ if (referrer.id === user.id) return res.status(400).json({ error: 'Cannot refer yourself' });
+
+ // Check if this user was already referred
+ const { data: existing } = await supabase
+ .from('referrals')
+ .select('id')
+ .eq('referred_user_id', user.id)
+ .single();
+
+ if (existing) return res.status(400).json({ error: 'Already referred' });
+
+ // Insert referral record
+ const { error: insertErr } = await supabase
+ .from('referrals')
+ .insert({ referrer_id: referrer.id, referred_user_id: user.id, rewarded: true });
+
+ if (insertErr) throw insertErr;
+
+ // Add bonus searches to referrer
+ const currentBonus = await getBonusSearches(referrer.id);
+ await supabase
+ .from('bonus_searches')
+ .upsert({ user_id: referrer.id, bonus_count: currentBonus + REFERRAL_BONUS, updated_at: new Date().toISOString() });
+
+ res.json({ success: true });
+ } catch (error) {
+ console.error('Referral claim error:', error);
+ res.status(500).json({ error: 'Failed to process referral' });
+ }
+});
+
+app.get('/api/referral/validate/:code', async (req, res) => {
+ try {
+ const { code } = req.params;
+ const { data: allUsers } = await supabaseAdmin.auth.admin.listUsers();
+ const referrer = (allUsers?.users || []).find(
+ u => generateReferralCode(u.id) === code
+ );
+ res.json({ valid: !!referrer });
+ } catch {
+ res.json({ valid: false });
+ }
+});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
diff --git a/server/package-lock.json b/server/package-lock.json
index bca940d..d2bf9d6 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -11,6 +11,7 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.95.1",
"@google/generative-ai": "^0.24.1",
+ "@supabase/supabase-js": "^2.106.2",
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
@@ -62,6 +63,90 @@
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
"license": "MIT"
},
+ "node_modules/@supabase/auth-js": {
+ "version": "2.106.2",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.106.2.tgz",
+ "integrity": "sha512-VcAjUErkHkhC5Jaf+g/G1qbkQrFh8edaCdHa7pxJmHUjkWKjT7UnYCtPA89XV0N0GIYRkEqJZw5V62CtOxTmBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/functions-js": {
+ "version": "2.106.2",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.106.2.tgz",
+ "integrity": "sha512-oRnr0QrL8H+zTO1YyQ1QjiHZU/957jvubbxSJTUm2XLAgzoGGV9Tahfyd+uvLsBLRVmXLtpU3oyCjdQIvkGMOA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/phoenix": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.2.tgz",
+ "integrity": "sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A==",
+ "license": "MIT"
+ },
+ "node_modules/@supabase/postgrest-js": {
+ "version": "2.106.2",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.106.2.tgz",
+ "integrity": "sha512-tDOzyPgp9pIRMR2x6C9+uDSJrnXSzxLtt3d7nC+Lrsy3jnJDHYfdQC/xcRyhJE/TOBJ0heSqRKR3UmejDjZxsw==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/realtime-js": {
+ "version": "2.106.2",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.106.2.tgz",
+ "integrity": "sha512-LdRGT7DNhyZkPjubUv5bSdAZ0jSEX8wTHvx7htj7+K59TOZRvz4TuQK7tL2RWxyIZVeFMRluL04SzWS61rKnUA==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/phoenix": "^0.4.2",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/storage-js": {
+ "version": "2.106.2",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.106.2.tgz",
+ "integrity": "sha512-xgKCSYuev1YarV+iVqr+zlfgSyremnJtn8T0NCT8L4XmMv1CLtESc0Q6kNp8+mKWdX/8ND0nzm7OMKx08kwNAw==",
+ "license": "MIT",
+ "dependencies": {
+ "iceberg-js": "^0.8.1",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/supabase-js": {
+ "version": "2.106.2",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.106.2.tgz",
+ "integrity": "sha512-2/RZ/1fmJx/MRSEDG2Xk8+J4JVk5clM9V0uSI6kUTrcS32KA89DtqI5RUOC9r6mzY3WBC9qexLjssIHjbLyVJA==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/auth-js": "2.106.2",
+ "@supabase/functions-js": "2.106.2",
+ "@supabase/postgrest-js": "2.106.2",
+ "@supabase/realtime-js": "2.106.2",
+ "@supabase/storage-js": "2.106.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -496,6 +581,15 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/iceberg-js": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
+ "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
@@ -944,6 +1038,12 @@
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
"license": "MIT"
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
diff --git a/server/package.json b/server/package.json
index 02b793f..4984668 100644
--- a/server/package.json
+++ b/server/package.json
@@ -13,6 +13,7 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.95.1",
"@google/generative-ai": "^0.24.1",
+ "@supabase/supabase-js": "^2.106.2",
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",